added undo-redo using seperate component
This commit is contained in:
11
src/App.tsx
11
src/App.tsx
@@ -1,8 +1,11 @@
|
|||||||
|
import { UndoRedoProvider } from "./UndoRedoContext";
|
||||||
import SceneView from "./pages/SceneView";
|
import SceneView from "./pages/SceneView";
|
||||||
import "./styles/main.scss";
|
import "./styles/main.scss";
|
||||||
|
|
||||||
function App() {
|
export default function App() {
|
||||||
return <SceneView />;
|
return (
|
||||||
|
<UndoRedoProvider>
|
||||||
|
<SceneView />
|
||||||
|
</UndoRedoProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|||||||
95
src/UndoRedoContext.tsx
Normal file
95
src/UndoRedoContext.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||||
|
import { UndoRedoContext, type UndoRedoAction } from "./useUndoRedo";
|
||||||
|
|
||||||
|
export const UndoRedoProvider = ({
|
||||||
|
children,
|
||||||
|
onUndo,
|
||||||
|
onRedo,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
onUndo?: () => void;
|
||||||
|
onRedo?: () => void;
|
||||||
|
}) => {
|
||||||
|
const undoStack = useRef<UndoRedoAction[]>([]);
|
||||||
|
const redoStack = useRef<UndoRedoAction[]>([]);
|
||||||
|
|
||||||
|
const [undoShortcut, setUndoShortcut] = useState("z");
|
||||||
|
const [redoShortcut, setRedoShortcut] = useState("y");
|
||||||
|
const [renderTick, setRenderTick] = useState(0); // trigger UI updates
|
||||||
|
|
||||||
|
const addAction = useCallback((action: UndoRedoAction) => {
|
||||||
|
undoStack.current.push(action);
|
||||||
|
redoStack.current = []; // clear redo stack
|
||||||
|
setRenderTick((t) => t + 1);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const undo = useCallback(() => {
|
||||||
|
const action = undoStack.current.pop();
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
action.undo(); // Call the undo method
|
||||||
|
redoStack.current.push(action);
|
||||||
|
setRenderTick((t) => t + 1);
|
||||||
|
onUndo?.();
|
||||||
|
}, [onUndo]);
|
||||||
|
|
||||||
|
const redo = useCallback(() => {
|
||||||
|
const action = redoStack.current.pop();
|
||||||
|
if (!action) return;
|
||||||
|
|
||||||
|
action.do(); // Call the do method (redo action)
|
||||||
|
undoStack.current.push(action);
|
||||||
|
setRenderTick((t) => t + 1);
|
||||||
|
onRedo?.();
|
||||||
|
}, [onRedo]);
|
||||||
|
|
||||||
|
const setShortcutKeys = useCallback((undoKey?: string, redoKey?: string) => {
|
||||||
|
if (undoKey) setUndoShortcut(undoKey);
|
||||||
|
if (redoKey) setRedoShortcut(redoKey);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// safer macOS detection (avoid deprecated navigator.platform)
|
||||||
|
const isMac = useMemo(() => {
|
||||||
|
if (typeof navigator === "undefined") return false;
|
||||||
|
return /Mac|iPhone|iPad|iPod/i.test(navigator.userAgent);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
const ctrlOrCmd = isMac ? e.metaKey : e.ctrlKey;
|
||||||
|
|
||||||
|
if (ctrlOrCmd && e.key.toLowerCase() === undoShortcut) {
|
||||||
|
e.preventDefault();
|
||||||
|
undo();
|
||||||
|
} else if (ctrlOrCmd && e.shiftKey && e.key.toLowerCase() === redoShortcut) {
|
||||||
|
e.preventDefault();
|
||||||
|
redo();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("keydown", handler);
|
||||||
|
return () => window.removeEventListener("keydown", handler);
|
||||||
|
}, [undoShortcut, redoShortcut, undo, redo, isMac]);
|
||||||
|
|
||||||
|
// 🧠 Memoize context value to prevent re-creation each render
|
||||||
|
const contextValue = useMemo(
|
||||||
|
() => ({
|
||||||
|
addAction,
|
||||||
|
undo,
|
||||||
|
redo,
|
||||||
|
canUndo: undoStack.current.length > 0,
|
||||||
|
canRedo: redoStack.current.length > 0,
|
||||||
|
stackCount: {
|
||||||
|
undo: undoStack.current.length,
|
||||||
|
redo: redoStack.current.length,
|
||||||
|
},
|
||||||
|
setShortcutKeys,
|
||||||
|
}),
|
||||||
|
// force update when renderTick changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[renderTick, addAction, undo, redo, setShortcutKeys]
|
||||||
|
);
|
||||||
|
|
||||||
|
return <UndoRedoContext.Provider value={contextValue}>{children}</UndoRedoContext.Provider>;
|
||||||
|
};
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import React, { useCallback, useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { AddIcon, ChevronIcon, CollapseAllIcon } from "../../icons/ExportIcons";
|
|
||||||
|
|
||||||
import TreeNode from "./TreeNode";
|
import TreeNode from "./TreeNode";
|
||||||
import Search from "./Search";
|
import Search from "./Search";
|
||||||
|
import { AddIcon, ChevronIcon, CollapseAllIcon } from "../../icons/ExportIcons";
|
||||||
import type { OutlinePanelProps } from "./OutlinePanel";
|
import type { OutlinePanelProps } from "./OutlinePanel";
|
||||||
|
import { useUndoRedo } from "../../useUndoRedo";
|
||||||
|
|
||||||
import { DEFAULT_PRAMS, type AssetGroupChild } from "../../data/OutlineListData";
|
import { DEFAULT_PRAMS, type AssetGroupChild } from "../../data/OutlineListData";
|
||||||
|
|
||||||
type DropAction = "above" | "child" | "below" | "none";
|
type DropAction = "above" | "child" | "below" | "none";
|
||||||
@@ -19,6 +21,10 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
width,
|
width,
|
||||||
search,
|
search,
|
||||||
data,
|
data,
|
||||||
|
onDrop,
|
||||||
|
onDragOver,
|
||||||
|
onRename,
|
||||||
|
onDragStart,
|
||||||
}) => {
|
}) => {
|
||||||
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
const [isPanelOpen, setIsPanelOpen] = useState(true);
|
||||||
const [draggedNode, setDraggedNode] = useState<AssetGroupChild | null>(null);
|
const [draggedNode, setDraggedNode] = useState<AssetGroupChild | null>(null);
|
||||||
@@ -27,36 +33,7 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
const [selectedObject, setSelectedObject] = useState<AssetGroupChild | null>(null);
|
const [selectedObject, setSelectedObject] = useState<AssetGroupChild | null>(null);
|
||||||
const [selectedObjects, setSelectedObjects] = useState<AssetGroupChild[]>([]);
|
const [selectedObjects, setSelectedObjects] = useState<AssetGroupChild[]>([]);
|
||||||
const [draggedItems, setDraggedItems] = useState<AssetGroupChild[]>([]);
|
const [draggedItems, setDraggedItems] = useState<AssetGroupChild[]>([]);
|
||||||
const [undoStack, setUndoStack] = useState<AssetGroupChild[][]>([]);
|
const { addAction, undo, redo, canUndo, canRedo } = useUndoRedo();
|
||||||
const [redoStack, setRedoStack] = useState<AssetGroupChild[][]>([]);
|
|
||||||
|
|
||||||
const pushHistory = (newHierarchy: AssetGroupChild[]) => {
|
|
||||||
setUndoStack((prev) => [...prev, structuredClone(hierarchy)]);
|
|
||||||
setRedoStack([]);
|
|
||||||
setHierarchy(newHierarchy);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUndo = useCallback(() => {
|
|
||||||
if (undoStack.length === 0) return;
|
|
||||||
|
|
||||||
const lastState = undoStack.at(-1);
|
|
||||||
if (!lastState) return;
|
|
||||||
|
|
||||||
setRedoStack((prev) => [...prev, structuredClone(hierarchy)]);
|
|
||||||
setHierarchy(lastState);
|
|
||||||
setUndoStack((prev) => prev.slice(0, -1));
|
|
||||||
}, [undoStack, hierarchy]);
|
|
||||||
|
|
||||||
const handleRedo = useCallback(() => {
|
|
||||||
if (redoStack.length === 0) return;
|
|
||||||
|
|
||||||
const nextState = redoStack.at(-1);
|
|
||||||
if (!nextState) return;
|
|
||||||
|
|
||||||
setUndoStack((prev) => [...prev, structuredClone(hierarchy)]);
|
|
||||||
setHierarchy(nextState);
|
|
||||||
setRedoStack((prev) => prev.slice(0, -1));
|
|
||||||
}, [redoStack, hierarchy]);
|
|
||||||
|
|
||||||
const handleDragStart = (item: AssetGroupChild) => {
|
const handleDragStart = (item: AssetGroupChild) => {
|
||||||
setDraggedNode(item);
|
setDraggedNode(item);
|
||||||
@@ -67,13 +44,10 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
: [item];
|
: [item];
|
||||||
|
|
||||||
setDraggedItems(itemsToDrag);
|
setDraggedItems(itemsToDrag);
|
||||||
|
if (onDragStart) onDragStart();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDragOver = (
|
const handleDragOver = (targetItem: AssetGroupChild, event: React.DragEvent) => {
|
||||||
targetItem: AssetGroupChild,
|
|
||||||
draggedItem: AssetGroupChild | null,
|
|
||||||
event: React.DragEvent
|
|
||||||
) => {
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const targetId = targetItem.modelUuid || targetItem.groupUuid;
|
const targetId = targetItem.modelUuid || targetItem.groupUuid;
|
||||||
if (!targetId) return;
|
if (!targetId) return;
|
||||||
@@ -88,6 +62,7 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
const rect = hoveredDiv.getBoundingClientRect();
|
const rect = hoveredDiv.getBoundingClientRect();
|
||||||
const y = event.clientY - rect.top;
|
const y = event.clientY - rect.top;
|
||||||
const dropZone = getDropZone(y);
|
const dropZone = getDropZone(y);
|
||||||
|
if (onDragOver) onDragOver();
|
||||||
|
|
||||||
switch (dropZone || selectedObject === null) {
|
switch (dropZone || selectedObject === null) {
|
||||||
case "above":
|
case "above":
|
||||||
@@ -130,29 +105,22 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedHierarchy = structuredClone(hierarchy);
|
const prevHierarchy = structuredClone(hierarchy);
|
||||||
|
const updatedHierarchy = [...hierarchy];
|
||||||
|
|
||||||
// ✅ Determine which items to move
|
if (!removeFromHierarchy(draggedItem, updatedHierarchy)) return;
|
||||||
const itemsToMove =
|
|
||||||
selectedObjects && selectedObjects.length > 1 ? selectedObjects : [draggedItem];
|
|
||||||
|
|
||||||
// ✅ Remove and insert each dragged item
|
if (!insertByDropAction(draggedItem, targetId, dropAction, updatedHierarchy)) {
|
||||||
itemsToMove.forEach((item) => {
|
updatedHierarchy.push(draggedItem);
|
||||||
if (!removeFromHierarchy(item, updatedHierarchy)) return;
|
}
|
||||||
|
addAction({
|
||||||
if (!insertByDropAction(item, targetId, dropAction, updatedHierarchy)) {
|
do: () => setHierarchy(updatedHierarchy),
|
||||||
updatedHierarchy.push(item);
|
undo: () => setHierarchy(prevHierarchy),
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
setHierarchy(updatedHierarchy);
|
||||||
// 🧠 Record new state for undo/redo
|
if (onDrop) onDrop(updatedHierarchy);
|
||||||
pushHistory(updatedHierarchy);
|
|
||||||
|
|
||||||
// ♻️ Cleanup
|
|
||||||
setDraggedNode(null);
|
setDraggedNode(null);
|
||||||
clearAllHighlights();
|
clearAllHighlights();
|
||||||
setSelectedObject(null);
|
|
||||||
setSelectedObjects([]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDropZone = (y: number): DropAction => {
|
const getDropZone = (y: number): DropAction => {
|
||||||
@@ -167,12 +135,12 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
".tree-node, .tree-node-content, .group-node, .dragging"
|
".tree-node, .tree-node-content, .group-node, .dragging"
|
||||||
);
|
);
|
||||||
|
|
||||||
allNodes.forEach((node) => {
|
for (const node of allNodes) {
|
||||||
node.style.borderTop = "none";
|
node.style.borderTop = "none";
|
||||||
node.style.borderBottom = "none";
|
node.style.borderBottom = "none";
|
||||||
node.style.outline = "none";
|
node.style.outline = "none";
|
||||||
node.style.border = "none";
|
node.style.border = "none";
|
||||||
});
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFromHierarchy = (item: AssetGroupChild, tree: AssetGroupChild[]): boolean => {
|
const removeFromHierarchy = (item: AssetGroupChild, tree: AssetGroupChild[]): boolean => {
|
||||||
@@ -265,6 +233,7 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
const filteredHierarchy = filterHierarchy(hierarchy, searchValue);
|
const filteredHierarchy = filterHierarchy(hierarchy, searchValue);
|
||||||
|
|
||||||
const handleRename = (id: string, newName: string) => {
|
const handleRename = (id: string, newName: string) => {
|
||||||
|
const prevHierarchy = structuredClone(hierarchy);
|
||||||
const updateNodeName = (nodes: AssetGroupChild[]): AssetGroupChild[] =>
|
const updateNodeName = (nodes: AssetGroupChild[]): AssetGroupChild[] =>
|
||||||
nodes.map((node) => {
|
nodes.map((node) => {
|
||||||
if (node.modelUuid === id || node.groupUuid === id) {
|
if (node.modelUuid === id || node.groupUuid === id) {
|
||||||
@@ -283,23 +252,25 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const newHierarchy = updateNodeName(structuredClone(hierarchy));
|
const newHierarchy = updateNodeName(structuredClone(hierarchy));
|
||||||
pushHistory(newHierarchy); // record rename
|
if (onRename) onRename(id, newName);
|
||||||
|
addAction({
|
||||||
|
do: () => setHierarchy(newHierarchy),
|
||||||
|
undo: () => setHierarchy(prevHierarchy),
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.ctrlKey && e.key.toLowerCase() === "z") {
|
if (e.ctrlKey && e.key.toLowerCase() === "z") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleUndo();
|
undo();
|
||||||
} else if (e.ctrlKey && e.key.toLowerCase() === "y") {
|
} else if (e.ctrlKey && e.key.toLowerCase() === "y") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleRedo();
|
redo();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener("keydown", handleKeyDown);
|
document.addEventListener("keydown", handleKeyDown);
|
||||||
return () => document.removeEventListener("keydown", handleKeyDown);
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
||||||
}, [hierarchy, undoStack, redoStack, handleUndo, handleRedo]);
|
}, [undo, redo]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -328,10 +299,10 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
<CollapseAllIcon />
|
<CollapseAllIcon />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button onClick={handleUndo} disabled={undoStack.length === 0} title="Undo">
|
<button onClick={undo} disabled={!canUndo} title="Undo">
|
||||||
⏪
|
⏪
|
||||||
</button>
|
</button>
|
||||||
<button onClick={handleRedo} disabled={redoStack.length === 0} title="Redo">
|
<button onClick={redo} disabled={!canRedo} title="Redo">
|
||||||
🔁
|
🔁
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -370,7 +341,6 @@ export const OutlineList: React.FC<OutlinePanelProps> = ({
|
|||||||
setSelectedObjects={setSelectedObjects}
|
setSelectedObjects={setSelectedObjects}
|
||||||
selectedObjects={selectedObjects}
|
selectedObjects={selectedObjects}
|
||||||
onRename={handleRename}
|
onRename={handleRename}
|
||||||
pushHistory={pushHistory}
|
|
||||||
hierarchy={hierarchy}
|
hierarchy={hierarchy}
|
||||||
setHierarchy={setHierarchy}
|
setHierarchy={setHierarchy}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ export interface OutlinePanelProps {
|
|||||||
width?: string;
|
width?: string;
|
||||||
search?: boolean;
|
search?: boolean;
|
||||||
data: AssetGroupChild[];
|
data: AssetGroupChild[];
|
||||||
|
onDrop?: (updatedData: AssetGroupChild[]) => void;
|
||||||
|
onDragStart?: () => void;
|
||||||
|
onDragOver?: () => void;
|
||||||
|
onRename?: (id: string, newName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const OutlinePanel: React.FC<OutlinePanelProps> = (props) => {
|
const OutlinePanel: React.FC<OutlinePanelProps> = (props) => {
|
||||||
|
|||||||
@@ -20,17 +20,17 @@ interface TreeNodeProps {
|
|||||||
onDragStart: (item: AssetGroupChild) => void;
|
onDragStart: (item: AssetGroupChild) => void;
|
||||||
onRename: (id: string, item: string) => void;
|
onRename: (id: string, item: string) => void;
|
||||||
onDrop: (item: AssetGroupChild, parent: AssetGroupChild | null, e: React.DragEvent) => void;
|
onDrop: (item: AssetGroupChild, parent: AssetGroupChild | null, e: React.DragEvent) => void;
|
||||||
|
onDragOver: (item: AssetGroupChild, e: React.DragEvent) => void;
|
||||||
draggingItem: AssetGroupChild | null;
|
draggingItem: AssetGroupChild | null;
|
||||||
draggedItems: AssetGroupChild[] | [];
|
draggedItems: AssetGroupChild[] | [];
|
||||||
hierarchy: AssetGroupChild[] | [];
|
hierarchy: AssetGroupChild[] | [];
|
||||||
selectedObject: AssetGroupChild | null;
|
selectedObject: AssetGroupChild | null;
|
||||||
selectedObjects: AssetGroupChild[] | [];
|
selectedObjects: AssetGroupChild[] | [];
|
||||||
onDragOver: (item: AssetGroupChild, parent: AssetGroupChild | null, e: React.DragEvent) => void;
|
|
||||||
setSelectedObject: React.Dispatch<React.SetStateAction<AssetGroupChild | null>>;
|
setSelectedObject: React.Dispatch<React.SetStateAction<AssetGroupChild | null>>;
|
||||||
setSelectedObjects: React.Dispatch<React.SetStateAction<AssetGroupChild[] | []>>;
|
setSelectedObjects: React.Dispatch<React.SetStateAction<AssetGroupChild[] | []>>;
|
||||||
setHierarchy: React.Dispatch<React.SetStateAction<AssetGroupChild[] | []>>;
|
setHierarchy: React.Dispatch<React.SetStateAction<AssetGroupChild[] | []>>;
|
||||||
setDraggedItems: React.Dispatch<React.SetStateAction<AssetGroupChild[] | []>>;
|
setDraggedItems: React.Dispatch<React.SetStateAction<AssetGroupChild[] | []>>;
|
||||||
pushHistory: (newHierarchy: AssetGroupChild[]) => void;
|
pushHistory?: (newHierarchy: AssetGroupChild[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TreeNode: React.FC<TreeNodeProps> = ({
|
const TreeNode: React.FC<TreeNodeProps> = ({
|
||||||
@@ -67,7 +67,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||||||
|
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
onDragOver(item, draggingItem, e);
|
onDragOver(item, e);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
@@ -158,7 +158,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
toggleVisibilityRecursive(updatedHierarchy);
|
toggleVisibilityRecursive(updatedHierarchy);
|
||||||
pushHistory(updatedHierarchy);
|
pushHistory?.(updatedHierarchy);
|
||||||
setHierarchy(updatedHierarchy);
|
setHierarchy(updatedHierarchy);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -180,7 +180,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
toggleLockRecursive(updatedHierarchy);
|
toggleLockRecursive(updatedHierarchy);
|
||||||
pushHistory(updatedHierarchy);
|
pushHistory?.(updatedHierarchy);
|
||||||
setHierarchy(updatedHierarchy);
|
setHierarchy(updatedHierarchy);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -326,7 +326,6 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Children */}
|
|
||||||
{isExpanded && item.children?.length ? (
|
{isExpanded && item.children?.length ? (
|
||||||
<div className="tree-children">
|
<div className="tree-children">
|
||||||
{item.children.map((child) => (
|
{item.children.map((child) => (
|
||||||
@@ -345,7 +344,7 @@ const TreeNode: React.FC<TreeNodeProps> = ({
|
|||||||
onRename={onRename}
|
onRename={onRename}
|
||||||
draggedItems={draggedItems}
|
draggedItems={draggedItems}
|
||||||
setDraggedItems={setDraggedItems}
|
setDraggedItems={setDraggedItems}
|
||||||
pushHistory={pushHistory}
|
// pushHistory={pushHistory}
|
||||||
setHierarchy={setHierarchy}
|
setHierarchy={setHierarchy}
|
||||||
hierarchy={hierarchy}
|
hierarchy={hierarchy}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,18 +1,48 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import OutlinePanel from "../components/ui/OutlinePanel";
|
import OutlinePanel from "../components/ui/OutlinePanel";
|
||||||
import { OutlineListData } from "../data/OutlineListData";
|
import { OutlineListData, type AssetGroupChild } from "../data/OutlineListData";
|
||||||
|
|
||||||
const SceneView = () => {
|
const SceneView = () => {
|
||||||
|
const [outlineData, setOutlineData] = useState<AssetGroupChild[] | []>(OutlineListData);
|
||||||
|
|
||||||
|
const handleDragStart = () => {};
|
||||||
|
|
||||||
|
const handleDragOver = () => {};
|
||||||
|
|
||||||
|
const handleDrop = (updatedData: AssetGroupChild[]) => {
|
||||||
|
setOutlineData(updatedData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = (id: string, newName: string) => {
|
||||||
|
const renameItem = (items: AssetGroupChild[]): AssetGroupChild[] => {
|
||||||
|
return items.map((item) => {
|
||||||
|
if (item.modelUuid === id) {
|
||||||
|
return { ...item, modelName: newName };
|
||||||
|
} else if (item.children && item.children.length > 0) {
|
||||||
|
return { ...item, children: renameItem(item.children) };
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const updatedData = renameItem(outlineData);
|
||||||
|
setOutlineData(updatedData);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OutlinePanel
|
<OutlinePanel
|
||||||
// height="500px"
|
height="500px"
|
||||||
// width="420px"
|
width="420px"
|
||||||
panelSide="right"
|
panelSide="right"
|
||||||
// textColor=""
|
textColor=""
|
||||||
// addIconColor=""
|
addIconColor=""
|
||||||
// eyeIconColor=""
|
eyeIconColor=""
|
||||||
// backgroundColor=""
|
backgroundColor=""
|
||||||
// search={true}
|
search={true}
|
||||||
data={OutlineListData}
|
data={outlineData}
|
||||||
|
onDrop={handleDrop}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onRename={handleRename}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
26
src/useUndoRedo.tsx
Normal file
26
src/useUndoRedo.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
|
||||||
|
export type UndoRedoAction = {
|
||||||
|
undo: () => void;
|
||||||
|
do: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UndoRedoContextType = {
|
||||||
|
addAction: (action: UndoRedoAction) => void;
|
||||||
|
undo: () => void;
|
||||||
|
redo: () => void;
|
||||||
|
canUndo: boolean;
|
||||||
|
canRedo: boolean;
|
||||||
|
stackCount: { undo: number; redo: number };
|
||||||
|
setShortcutKeys?: (undoKey?: string, redoKey?: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UndoRedoContext = createContext<UndoRedoContextType | null>(null);
|
||||||
|
|
||||||
|
export const useUndoRedo = () => {
|
||||||
|
const ctx = useContext(UndoRedoContext);
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error("useUndoRedo must be used inside an UndoRedoProvider");
|
||||||
|
}
|
||||||
|
return ctx;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user