feat: Add interaction toolbar and block count HUD to Map3D component
This commit is contained in:
@@ -1282,16 +1282,133 @@ function Scene({ interactionMode, onInteraction }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Interaction mode toolbar overlay
|
||||||
|
function InteractionToolbar({ mode, setMode, lastInteraction }) {
|
||||||
|
const modes = [
|
||||||
|
{ key: INTERACTION_MODE.LOOK, icon: '👁️', label: 'Look', desc: 'Orbit camera' },
|
||||||
|
{ key: INTERACTION_MODE.MOVE, icon: '🎯', label: 'Move', desc: 'Click to send turtle' },
|
||||||
|
{ key: INTERACTION_MODE.BUILD, icon: '🧱', label: 'Build', desc: 'Click face to preview' },
|
||||||
|
{ key: INTERACTION_MODE.SELECT, icon: '⬜', label: 'Select', desc: 'Click two corners' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '16px',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
display: 'flex',
|
||||||
|
gap: '4px',
|
||||||
|
background: 'rgba(15, 23, 42, 0.9)',
|
||||||
|
padding: '6px',
|
||||||
|
borderRadius: '12px',
|
||||||
|
border: '1px solid rgba(100, 116, 139, 0.3)',
|
||||||
|
zIndex: 10,
|
||||||
|
backdropFilter: 'blur(10px)',
|
||||||
|
}}>
|
||||||
|
{modes.map(m => (
|
||||||
|
<button
|
||||||
|
key={m.key}
|
||||||
|
onClick={() => setMode(m.key)}
|
||||||
|
title={m.desc}
|
||||||
|
style={{
|
||||||
|
padding: '8px 14px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: mode === m.key ? '2px solid #3b82f6' : '1px solid transparent',
|
||||||
|
background: mode === m.key ? 'rgba(59, 130, 246, 0.2)' : 'rgba(30, 41, 59, 0.8)',
|
||||||
|
color: mode === m.key ? '#60a5fa' : '#94a3b8',
|
||||||
|
cursor: 'pointer',
|
||||||
|
fontSize: '13px',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '6px',
|
||||||
|
transition: 'all 0.15s ease',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span style={{ fontSize: '16px' }}>{m.icon}</span>
|
||||||
|
{m.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{lastInteraction && (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '100%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
marginBottom: '8px',
|
||||||
|
padding: '6px 12px',
|
||||||
|
background: 'rgba(15, 23, 42, 0.95)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid rgba(100, 116, 139, 0.3)',
|
||||||
|
color: '#94a3b8',
|
||||||
|
fontSize: '12px',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}>
|
||||||
|
{lastInteraction.mode === 'move' && `🎯 Moving turtle to (${lastInteraction.target.x}, ${lastInteraction.target.y}, ${lastInteraction.target.z})`}
|
||||||
|
{lastInteraction.mode === 'build' && `🧱 Build at (${lastInteraction.target.x}, ${lastInteraction.target.y}, ${lastInteraction.target.z})`}
|
||||||
|
{lastInteraction.mode === 'select' && lastInteraction.end &&
|
||||||
|
`⬜ Selected (${lastInteraction.start.x},${lastInteraction.start.y},${lastInteraction.start.z}) → (${lastInteraction.end.x},${lastInteraction.end.y},${lastInteraction.end.z})`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block count HUD
|
||||||
|
function BlockCountHUD({ blocks }) {
|
||||||
|
const count = blocks.length;
|
||||||
|
const chunkCount = useMemo(() => {
|
||||||
|
const chunks = new Set();
|
||||||
|
blocks.forEach(b => chunks.add(`${Math.floor(b.x / 16)},${Math.floor(b.z / 16)}`));
|
||||||
|
return chunks.size;
|
||||||
|
}, [blocks]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '12px',
|
||||||
|
right: '12px',
|
||||||
|
padding: '8px 12px',
|
||||||
|
background: 'rgba(15, 23, 42, 0.85)',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid rgba(100, 116, 139, 0.2)',
|
||||||
|
color: '#94a3b8',
|
||||||
|
fontSize: '12px',
|
||||||
|
zIndex: 10,
|
||||||
|
}}>
|
||||||
|
🧊 {count.toLocaleString()} blocks · 📦 {chunkCount} chunks
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Main Map3D component
|
// Main Map3D component
|
||||||
export default function Map3D() {
|
export default function Map3D() {
|
||||||
|
const [interactionMode, setInteractionMode] = useState(INTERACTION_MODE.LOOK);
|
||||||
|
const [lastInteraction, setLastInteraction] = useState(null);
|
||||||
|
const worldBlocks = useTurtleStore((state) => state.worldBlocks || []);
|
||||||
|
|
||||||
|
const handleInteraction = useCallback((event) => {
|
||||||
|
setLastInteraction(event);
|
||||||
|
// Clear notification after 5 seconds
|
||||||
|
setTimeout(() => setLastInteraction(null), 5000);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width: '100%', height: '100%', background: '#0a0e1a' }}>
|
<div style={{ width: '100%', height: '100%', background: '#0a0e1a', position: 'relative' }}>
|
||||||
<Canvas
|
<Canvas
|
||||||
camera={{ position: [15, 15, 15], fov: 60 }}
|
camera={{ position: [15, 15, 15], fov: 60 }}
|
||||||
style={{ background: '#0a0e1a' }}
|
style={{ background: '#0a0e1a' }}
|
||||||
>
|
>
|
||||||
<Scene />
|
<Scene interactionMode={interactionMode} onInteraction={handleInteraction} />
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
|
<BlockCountHUD blocks={worldBlocks} />
|
||||||
|
<InteractionToolbar
|
||||||
|
mode={interactionMode}
|
||||||
|
setMode={setInteractionMode}
|
||||||
|
lastInteraction={lastInteraction}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user