cleaner add + undo/redo state
This commit is contained in:
@@ -37,6 +37,7 @@ export default function Editor() {
|
|||||||
<div className="w-full h-full bg-pink-50">
|
<div className="w-full h-full bg-pink-50">
|
||||||
{/* <LevelModeSwitcher /> */}
|
{/* <LevelModeSwitcher /> */}
|
||||||
|
|
||||||
|
<TestUndo />
|
||||||
<ActionMenu />
|
<ActionMenu />
|
||||||
<Viewer>
|
<Viewer>
|
||||||
<DraftSelector />
|
<DraftSelector />
|
||||||
@@ -49,6 +50,31 @@ export default function Editor() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TestUndo = () => {
|
||||||
|
const {undo, redo, futureStates, pastStates} = useScene.temporal.getState();
|
||||||
|
|
||||||
|
|
||||||
|
return ( <div className="absolute top-4 right-4 z-10 flex gap-2">
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 rounded bg-white"
|
||||||
|
onClick={() => {
|
||||||
|
undo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Undo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="px-4 py-2 rounded bg-white"
|
||||||
|
onClick={() => {
|
||||||
|
redo();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Redo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const Passes = ({}) => {
|
export const Passes = ({}) => {
|
||||||
const { gl: renderer, scene, camera } = useThree();
|
const { gl: renderer, scene, camera } = useThree();
|
||||||
const postProcessingRef = useRef(null);
|
const postProcessingRef = useRef(null);
|
||||||
|
|||||||
@@ -56,6 +56,7 @@
|
|||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"zod": "^4.3.5",
|
"zod": "^4.3.5",
|
||||||
|
"zundo": "^2.3.0",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
@@ -1072,6 +1073,8 @@
|
|||||||
|
|
||||||
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
"zod": ["zod@4.3.5", "", {}, "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g=="],
|
||||||
|
|
||||||
|
"zundo": ["zundo@2.3.0", "", { "peerDependencies": { "zustand": "^4.3.0 || ^5.0.0" } }, "sha512-4GXYxXA17SIKYhVbWHdSEU04P697IMyVGXrC2TnzoyohEAWytFNOKqOp5gTGvaW93F/PM5Y0evbGtOPF0PWQwQ=="],
|
||||||
|
|
||||||
"zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="],
|
"zustand": ["zustand@5.0.10", "", { "peerDependencies": { "@types/react": ">=18.0.0", "immer": ">=9.0.6", "react": ">=18.0.0", "use-sync-external-store": ">=1.2.0" }, "optionalPeers": ["@types/react", "immer", "react", "use-sync-external-store"] }, "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg=="],
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"dedent": "^1.7.1",
|
"dedent": "^1.7.1",
|
||||||
"mitt": "^3.0.1",
|
"mitt": "^3.0.1",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.6",
|
||||||
"zod": "^4.3.5"
|
"zod": "^4.3.5",
|
||||||
|
"zundo": "^2.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { BuildingNode, ItemNode } from "../schema";
|
|||||||
import { LevelNode } from "../schema/nodes/level";
|
import { LevelNode } from "../schema/nodes/level";
|
||||||
import { WallNode } from "../schema/nodes/wall";
|
import { WallNode } from "../schema/nodes/wall";
|
||||||
import { AnyNode, AnyNodeId } from "../schema/types";
|
import { AnyNode, AnyNodeId } from "../schema/types";
|
||||||
|
import { temporal } from "zundo";
|
||||||
|
|
||||||
type SceneState = {
|
type SceneState = {
|
||||||
// 1. The Data: A flat dictionary of all nodes
|
// 1. The Data: A flat dictionary of all nodes
|
||||||
@@ -22,9 +23,13 @@ type SceneState = {
|
|||||||
clearDirty: (id: AnyNodeId) => void;
|
clearDirty: (id: AnyNodeId) => void;
|
||||||
|
|
||||||
createNode: (node: AnyNode, parentId?: AnyNodeId) => void;
|
createNode: (node: AnyNode, parentId?: AnyNodeId) => void;
|
||||||
|
createNodes: (ops: { node: AnyNode; parentId?: AnyNodeId }[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const useScene = create<SceneState>()((set, get) => ({
|
type PartializedStoreState = Pick<SceneState, 'rootNodeIds' | 'nodes'>;
|
||||||
|
|
||||||
|
const useScene = create<SceneState>()(temporal((set, get) => ({
|
||||||
|
|
||||||
// 1. Flat dictionary of all nodes
|
// 1. Flat dictionary of all nodes
|
||||||
nodes: {},
|
nodes: {},
|
||||||
|
|
||||||
@@ -126,27 +131,76 @@ const useScene = create<SceneState>()((set, get) => ({
|
|||||||
get().dirtyNodes.delete(id);
|
get().dirtyNodes.delete(id);
|
||||||
},
|
},
|
||||||
|
|
||||||
createNode: (node, parentId) => {
|
createNodes: (ops) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const newNodes = { ...state.nodes, [node.id]: node };
|
const nextNodes = { ...state.nodes };
|
||||||
|
const nextRootIds = [...state.rootNodeIds];
|
||||||
|
|
||||||
if (parentId) {
|
for (const { node, parentId } of ops) {
|
||||||
const parent = state.nodes[parentId];
|
// 1. Assign parentId to the child (Safe because BaseNode has parentId)
|
||||||
if (parent) {
|
const newNode = {
|
||||||
newNodes[parentId] = {
|
...node,
|
||||||
|
parentId: parentId ?? null
|
||||||
|
};
|
||||||
|
|
||||||
|
nextNodes[newNode.id] = newNode;
|
||||||
|
|
||||||
|
// 2. Update the Parent's children list
|
||||||
|
if (parentId && nextNodes[parentId]) {
|
||||||
|
const parent = nextNodes[parentId];
|
||||||
|
|
||||||
|
// Type Guard: Check if the parent node is a container that supports children
|
||||||
|
if ("children" in parent && Array.isArray(parent.children)) {
|
||||||
|
nextNodes[parentId] = {
|
||||||
...parent,
|
...parent,
|
||||||
children: [...parent.children, node.id],
|
// Use Set to prevent duplicate IDs if createNode is called twice
|
||||||
|
children: Array.from(new Set([...parent.children, newNode.id])) as any, // We don't verify child types here
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
} else if (!parentId) {
|
||||||
|
// 3. Handle Root nodes
|
||||||
|
if (!nextRootIds.includes(newNode.id)) {
|
||||||
|
nextRootIds.push(newNode.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { nodes: newNodes };
|
return { nodes: nextNodes, rootNodeIds: nextRootIds };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 4. System Sync
|
||||||
|
ops.forEach(({ node, parentId }) => {
|
||||||
get().markDirty(node.id);
|
get().markDirty(node.id);
|
||||||
if (parentId) get().markDirty(parentId);
|
if (parentId) get().markDirty(parentId);
|
||||||
}
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3. The CONVENIENCE (Singular)
|
||||||
|
createNode: (node, parentId) => get().createNodes([{ node, parentId }]),
|
||||||
|
|
||||||
|
|
||||||
|
}), {
|
||||||
|
partialize: (state) => {
|
||||||
|
const { nodes, rootNodeIds } = state; // Only track nodes and rootNodeIds in history
|
||||||
|
return { nodes, rootNodeIds}
|
||||||
|
},
|
||||||
|
limit: 50, // Limit to last 50 actions
|
||||||
}));
|
}));
|
||||||
|
|
||||||
useScene.getState().loadScene();
|
useScene.getState().loadScene();
|
||||||
export default useScene;
|
export default useScene;
|
||||||
|
|
||||||
|
|
||||||
|
// Subscribe to the temporal store (Undo/Redo events)
|
||||||
|
useScene.temporal.subscribe((state, prevState) => {
|
||||||
|
// Check if we just jumped in time (Undo/Redo)
|
||||||
|
// If the 'nodes' object changed but it wasn't a normal 'set'
|
||||||
|
const currentNodes = useScene.getState().nodes;
|
||||||
|
|
||||||
|
// Trigger a full scene re-validation
|
||||||
|
Object.values(currentNodes).forEach(node => {
|
||||||
|
if (node.type === 'wall') {
|
||||||
|
useScene.getState().markDirty(node.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user