voxel editor undo/redo
This commit is contained in:
parent
30ab3e4d7e
commit
52caee642e
|
|
@ -1,11 +1,11 @@
|
|||
import { Canvas } from '@react-three/fiber';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { useState, useCallback, useEffect, useRef } from 'react';
|
||||
import { state } from '../../state';
|
||||
import type { Voxel, V3 } from '../../types';
|
||||
import { VoxelEditorScene } from './VoxelEditorScene';
|
||||
import { IconArrowLeft, IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
import { IconArrowLeft, IconArrowBackUp, IconArrowForwardUp, IconPlus, IconTrash } from '@tabler/icons-react';
|
||||
|
||||
const PALETTE = [
|
||||
'#ffffff', '#cccccc', '#999999', '#555555', '#222222',
|
||||
|
|
@ -14,11 +14,19 @@ const PALETTE = [
|
|||
'#e8b820', '#484040', '#d0c8b8', '#808080', '#181414',
|
||||
];
|
||||
|
||||
type HistoryEntry =
|
||||
| { type: 'add'; voxel: Voxel }
|
||||
| { type: 'remove'; voxel: Voxel };
|
||||
|
||||
export const VoxelEditorPage = observer(function () {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [mode, setMode] = useState<'add' | 'remove'>('add');
|
||||
const [color, setColor] = useState('#808080');
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const history = useRef<HistoryEntry[]>([]);
|
||||
const future = useRef<HistoryEntry[]>([]);
|
||||
|
||||
const objectType = id ? state.world.getObjectTypeById(id) : undefined;
|
||||
|
||||
|
|
@ -28,14 +36,67 @@ export const VoxelEditorPage = observer(function () {
|
|||
};
|
||||
}, [id]);
|
||||
|
||||
const handleAdd = useCallback((voxel: Voxel) => {
|
||||
if (id) state.worldEditor.addVoxelToObjectType(id, voxel);
|
||||
function pushHistory(entry: HistoryEntry) {
|
||||
history.current.push(entry);
|
||||
future.current = [];
|
||||
setCanUndo(true);
|
||||
setCanRedo(false);
|
||||
}
|
||||
|
||||
const undo = useCallback(() => {
|
||||
const entry = history.current.pop();
|
||||
if (!entry || !id) return;
|
||||
if (entry.type === 'add') {
|
||||
state.worldEditor.removeVoxelFromObjectType(id, entry.voxel.position);
|
||||
} else {
|
||||
state.worldEditor.addVoxelToObjectType(id, entry.voxel);
|
||||
}
|
||||
future.current.push(entry);
|
||||
setCanUndo(history.current.length > 0);
|
||||
setCanRedo(true);
|
||||
}, [id]);
|
||||
|
||||
const handleRemove = useCallback((position: V3) => {
|
||||
if (id) state.worldEditor.removeVoxelFromObjectType(id, position);
|
||||
const redo = useCallback(() => {
|
||||
const entry = future.current.pop();
|
||||
if (!entry || !id) return;
|
||||
if (entry.type === 'add') {
|
||||
state.worldEditor.addVoxelToObjectType(id, entry.voxel);
|
||||
} else {
|
||||
state.worldEditor.removeVoxelFromObjectType(id, entry.voxel.position);
|
||||
}
|
||||
history.current.push(entry);
|
||||
setCanUndo(true);
|
||||
setCanRedo(future.current.length > 0);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
if (!e.ctrlKey) return;
|
||||
if (e.key === 'z' && !e.shiftKey) { e.preventDefault(); undo(); }
|
||||
if (e.key === 'y' || (e.key === 'z' && e.shiftKey)) { e.preventDefault(); redo(); }
|
||||
}
|
||||
window.addEventListener('keydown', onKeyDown);
|
||||
return () => window.removeEventListener('keydown', onKeyDown);
|
||||
}, [undo, redo]);
|
||||
|
||||
function handleAdd(voxel: Voxel) {
|
||||
if (!id) return;
|
||||
state.worldEditor.addVoxelToObjectType(id, voxel);
|
||||
pushHistory({ type: 'add', voxel });
|
||||
}
|
||||
|
||||
function handleRemove(position: V3) {
|
||||
if (!id || !objectType) return;
|
||||
const voxel = objectType.voxels.find(v =>
|
||||
v.position[0] === position[0] &&
|
||||
v.position[1] === position[1] &&
|
||||
v.position[2] === position[2]
|
||||
);
|
||||
if (!voxel) return;
|
||||
state.worldEditor.removeVoxelFromObjectType(id, position);
|
||||
pushHistory({ type: 'remove', voxel });
|
||||
}
|
||||
|
||||
if (!objectType) {
|
||||
return (
|
||||
<div style={{ color: 'white', padding: 24 }}>
|
||||
|
|
@ -47,15 +108,19 @@ export const VoxelEditorPage = observer(function () {
|
|||
return (
|
||||
<div style={{ position: 'fixed', inset: 0, background: '#1a1a2e', display: 'flex', flexDirection: 'column' }}>
|
||||
{/* Top bar */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '8px 12px', background: '#11111f', flexShrink: 0 }}>
|
||||
<button
|
||||
onClick={() => navigate(`/editor/objectType/${id}`)}
|
||||
style={btnStyle}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '8px 12px', background: '#11111f', flexShrink: 0 }}>
|
||||
<button onClick={() => navigate(`/editor/objectType/${id}`)} style={btnStyle}>
|
||||
<IconArrowLeft size={20} />
|
||||
</button>
|
||||
<span style={{ color: '#ccc', fontWeight: 600 }}>{objectType.name}</span>
|
||||
<span style={{ color: '#666', fontSize: 13 }}>{objectType.voxels.length} voxels</span>
|
||||
<div style={{ flex: 1 }} />
|
||||
<button onClick={undo} disabled={!canUndo} style={{ ...btnStyle, opacity: canUndo ? 1 : 0.35 }}>
|
||||
<IconArrowBackUp size={20} />
|
||||
</button>
|
||||
<button onClick={redo} disabled={!canRedo} style={{ ...btnStyle, opacity: canRedo ? 1 : 0.35 }}>
|
||||
<IconArrowForwardUp size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 3D canvas */}
|
||||
|
|
|
|||
Loading…
Reference in New Issue