新增画布舞台交互 hook,承接选择、框选、拖拽、平移和小地图 pointer 状态机 更新历史恢复清理入口,撤销重做时统一重置舞台交互状态 补充舞台交互 hook 测试并更新前端拆分文档和 TRACKING 记录
156 lines
4.9 KiB
TypeScript
156 lines
4.9 KiB
TypeScript
import { type RefObject, useCallback, useRef, useState } from 'react';
|
|
|
|
import { MAX_HISTORY_STEPS } from './ImageCanvasEditorModel';
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasHistorySnapshot,
|
|
CanvasLayer,
|
|
CanvasViewport,
|
|
GenerateDialogState,
|
|
} from './ImageCanvasEditorTypes';
|
|
|
|
type CanvasHistoryRefs = {
|
|
layersRef: RefObject<CanvasLayer[]>;
|
|
viewportRef: RefObject<CanvasViewport>;
|
|
generateDialogRef: RefObject<GenerateDialogState | null>;
|
|
inactiveGenerateDialogsRef: RefObject<CanvasGenerationDialogState[]>;
|
|
selectedLayerIdRef: RefObject<string | null>;
|
|
selectedLayerIdsRef: RefObject<string[]>;
|
|
};
|
|
|
|
type CanvasHistorySetters = {
|
|
setLayers: (layers: CanvasLayer[]) => void;
|
|
setViewport: (viewport: CanvasViewport) => void;
|
|
setGenerateDialog: (dialog: GenerateDialogState | null) => void;
|
|
setInactiveGenerateDialogs: (
|
|
dialogs: CanvasGenerationDialogState[],
|
|
) => void;
|
|
setSelectedLayerId: (layerId: string | null) => void;
|
|
setSelectedLayerIds: (layerIds: string[]) => void;
|
|
};
|
|
|
|
type CanvasHistoryResetters = {
|
|
setHoveredLayerId: (layerId: string | null) => void;
|
|
setMetadataLayer: (layer: CanvasLayer | null) => void;
|
|
resetCanvasInteractionState: () => void;
|
|
};
|
|
|
|
function cloneGenerateDialog(dialog: GenerateDialogState): GenerateDialogState {
|
|
return {
|
|
...dialog,
|
|
placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
|
|
};
|
|
}
|
|
|
|
function cloneCanvasGenerationDialog(
|
|
dialog: CanvasGenerationDialogState,
|
|
): CanvasGenerationDialogState {
|
|
return {
|
|
...dialog,
|
|
placeholder: dialog.placeholder ? { ...dialog.placeholder } : undefined,
|
|
};
|
|
}
|
|
|
|
export function useCanvasHistory({
|
|
refs,
|
|
setters,
|
|
resetters,
|
|
}: {
|
|
refs: CanvasHistoryRefs;
|
|
setters: CanvasHistorySetters;
|
|
resetters: CanvasHistoryResetters;
|
|
}) {
|
|
const undoStackRef = useRef<CanvasHistorySnapshot[]>([]);
|
|
const redoStackRef = useRef<CanvasHistorySnapshot[]>([]);
|
|
const [historyVersion, setHistoryVersion] = useState(0);
|
|
|
|
const getCanvasHistorySnapshot = useCallback(
|
|
(): CanvasHistorySnapshot => ({
|
|
layers: refs.layersRef.current.map((layer) => ({ ...layer })),
|
|
viewport: { ...refs.viewportRef.current },
|
|
generateDialog: refs.generateDialogRef.current
|
|
? cloneGenerateDialog(refs.generateDialogRef.current)
|
|
: null,
|
|
inactiveGenerateDialogs:
|
|
refs.inactiveGenerateDialogsRef.current.map(cloneCanvasGenerationDialog),
|
|
selectedLayerId: refs.selectedLayerIdRef.current,
|
|
selectedLayerIds: [...refs.selectedLayerIdsRef.current],
|
|
}),
|
|
[refs],
|
|
);
|
|
|
|
const restoreCanvasHistorySnapshot = useCallback(
|
|
(snapshot: CanvasHistorySnapshot) => {
|
|
setters.setLayers(snapshot.layers.map((layer) => ({ ...layer })));
|
|
setters.setViewport({ ...snapshot.viewport });
|
|
setters.setGenerateDialog(
|
|
snapshot.generateDialog
|
|
? cloneGenerateDialog(snapshot.generateDialog)
|
|
: null,
|
|
);
|
|
setters.setInactiveGenerateDialogs(
|
|
snapshot.inactiveGenerateDialogs.map(cloneCanvasGenerationDialog),
|
|
);
|
|
setters.setSelectedLayerId(snapshot.selectedLayerId);
|
|
setters.setSelectedLayerIds([...snapshot.selectedLayerIds]);
|
|
resetters.setHoveredLayerId(null);
|
|
resetters.setMetadataLayer(null);
|
|
resetters.resetCanvasInteractionState();
|
|
},
|
|
[resetters, setters],
|
|
);
|
|
|
|
const captureCanvasHistory = useCallback(
|
|
(options: { clearRedo?: boolean } = {}) => {
|
|
undoStackRef.current = [
|
|
...undoStackRef.current.slice(-(MAX_HISTORY_STEPS - 1)),
|
|
getCanvasHistorySnapshot(),
|
|
];
|
|
if (options.clearRedo !== false) {
|
|
redoStackRef.current = [];
|
|
}
|
|
setHistoryVersion((version) => version + 1);
|
|
},
|
|
[getCanvasHistorySnapshot],
|
|
);
|
|
|
|
const undoCanvasChange = useCallback(() => {
|
|
const previousSnapshot = undoStackRef.current.at(-1);
|
|
if (!previousSnapshot) {
|
|
return;
|
|
}
|
|
undoStackRef.current = undoStackRef.current.slice(0, -1);
|
|
redoStackRef.current = [
|
|
...redoStackRef.current.slice(-(MAX_HISTORY_STEPS - 1)),
|
|
getCanvasHistorySnapshot(),
|
|
];
|
|
restoreCanvasHistorySnapshot(previousSnapshot);
|
|
setHistoryVersion((version) => version + 1);
|
|
}, [getCanvasHistorySnapshot, restoreCanvasHistorySnapshot]);
|
|
|
|
const redoCanvasChange = useCallback(() => {
|
|
const nextSnapshot = redoStackRef.current.at(-1);
|
|
if (!nextSnapshot) {
|
|
return;
|
|
}
|
|
redoStackRef.current = redoStackRef.current.slice(0, -1);
|
|
undoStackRef.current = [
|
|
...undoStackRef.current.slice(-(MAX_HISTORY_STEPS - 1)),
|
|
getCanvasHistorySnapshot(),
|
|
];
|
|
restoreCanvasHistorySnapshot(nextSnapshot);
|
|
setHistoryVersion((version) => version + 1);
|
|
}, [getCanvasHistorySnapshot, restoreCanvasHistorySnapshot]);
|
|
|
|
return {
|
|
canUndo: undoStackRef.current.length > 0,
|
|
canRedo: redoStackRef.current.length > 0,
|
|
historyVersion,
|
|
getCanvasHistorySnapshot,
|
|
restoreCanvasHistorySnapshot,
|
|
captureCanvasHistory,
|
|
undoCanvasChange,
|
|
redoCanvasChange,
|
|
};
|
|
}
|