Files
Genarrative/src/components/image-editor/useImageCanvasAssetCanvasBridge.ts
T
zhaojingyan eb608ac6f2 完善图片画布撤销保护与操作提示 (#89)
为画布历史记录补充操作类型和内容消失保护

增加撤销成功、恢复成功及阻止提示并在三秒后隐藏

补充拖动、上传、生成和替换等操作的历史语义

保留恢复按钮、Ctrl+Shift+Z 快捷键和双向历史栈

补充撤销与恢复规则的定向测试和说明文档

---------

Co-authored-by: 段舒康 <kdletters@qq.com>
Co-authored-by: kdletters <kdletters@qq.com>
Reviewed-on: https://git.genarrative.world/git/GenarrativeAI/Genarrative/pulls/89
Co-authored-by: oj-afraid-student <1373241747@qq.com>
Co-committed-by: oj-afraid-student <1373241747@qq.com>
2026-07-20 15:19:48 +08:00

218 lines
6.2 KiB
TypeScript

import {
type Dispatch,
type MutableRefObject,
type RefObject,
type SetStateAction,
useCallback,
useMemo,
} from 'react';
import {
createLayerFromAsset,
isLayerLinkedToAsset,
} from './ImageCanvasEditorModel';
import type {
AssetPointerDragState,
CanvasHistoryAction,
CanvasLayer,
CanvasViewport,
EditorAsset,
EditorAssetFolder,
} from './ImageCanvasEditorTypes';
import { useImageCanvasAssetPointerDragBridge } from './useImageCanvasAssetPointerDragBridge';
import { useImageCanvasCanvasDropWorkflow } from './useImageCanvasCanvasDropWorkflow';
type CanvasPoint = { x: number; y: number };
type UploadFilesToCanvasOptions = {
folderId?: string;
canvasPoint: CanvasPoint;
addToCanvas: true;
};
type UseImageCanvasAssetLayerCleanupOptions = {
layers: CanvasLayer[];
setLayers: Dispatch<SetStateAction<CanvasLayer[]>>;
setSelectedLayerId: Dispatch<SetStateAction<string | null>>;
setSelectedLayerIds: Dispatch<SetStateAction<string[]>>;
onDeleteLayerSideEffects?: (layerId: string) => void;
discardHistoryEntriesContainingLayer?: (
matchesLayer: (layer: CanvasLayer) => boolean,
) => void;
};
type UseImageCanvasAssetCanvasBridgeOptions = {
assetPointerDragRef: RefObject<AssetPointerDragState | null>;
suppressAssetClickRef: RefObject<boolean>;
assets: EditorAsset[];
assetFolders: EditorAssetFolder[];
layerCounterRef: MutableRefObject<number>;
viewport: CanvasViewport;
canvasSize: { width: number; height: number };
resolveAssetFolderId: (clientX: number, clientY: number) => string | null;
resolveCanvasPoint: (clientX: number, clientY: number) => CanvasPoint | null;
getCanvasDropPoint: (clientX: number, clientY: number) => CanvasPoint;
setAssetPointerDrag: Dispatch<SetStateAction<AssetPointerDragState | null>>;
setActiveUploadFolderId: Dispatch<SetStateAction<string>>;
setUploadDropTarget: Dispatch<SetStateAction<'canvas' | 'assets' | null>>;
setHoveredLayerId: Dispatch<SetStateAction<string | null>>;
updateAssetMoveDropFolder: (folderId: string | null) => void;
moveAssetToFolder: (assetId: string, folderId: string) => void;
captureCanvasHistory: (action: CanvasHistoryAction) => void;
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
selectSingleLayer: (layerId: string | null) => void;
addUploadedFiles: (
files: FileList | File[],
options: UploadFilesToCanvasOptions,
) => void;
};
export function useImageCanvasAssetLayerCleanup({
layers,
setLayers,
setSelectedLayerId,
setSelectedLayerIds,
onDeleteLayerSideEffects,
discardHistoryEntriesContainingLayer,
}: UseImageCanvasAssetLayerCleanupOptions) {
return useCallback(
(deletedAssets: EditorAsset[]) => {
if (!deletedAssets.length) {
return;
}
discardHistoryEntriesContainingLayer?.((layer) =>
deletedAssets.some((asset) => isLayerLinkedToAsset(layer, asset)),
);
const deletedLayerIds = layers
.filter((layer) =>
deletedAssets.some((asset) => isLayerLinkedToAsset(layer, asset)),
)
.map((layer) => layer.id);
setLayers((currentLayers) =>
currentLayers.filter(
(layer) =>
!deletedAssets.some((asset) => isLayerLinkedToAsset(layer, asset)),
),
);
setSelectedLayerIds((currentIds) =>
currentIds.filter((layerId) =>
layers.every(
(layer) =>
layer.id !== layerId ||
!deletedAssets.some((asset) =>
isLayerLinkedToAsset(layer, asset),
),
),
),
);
setSelectedLayerId((currentId) => {
if (!currentId) {
return currentId;
}
const currentLayer = layers.find((layer) => layer.id === currentId);
return currentLayer &&
deletedAssets.some((asset) => isLayerLinkedToAsset(currentLayer, asset))
? null
: currentId;
});
deletedLayerIds.forEach((layerId) =>
onDeleteLayerSideEffects?.(layerId),
);
},
[
discardHistoryEntriesContainingLayer,
layers,
onDeleteLayerSideEffects,
setLayers,
setSelectedLayerId,
setSelectedLayerIds,
],
);
}
export function useImageCanvasAssetCanvasBridge({
assetPointerDragRef,
suppressAssetClickRef,
assets,
assetFolders,
layerCounterRef,
viewport,
canvasSize,
resolveAssetFolderId,
resolveCanvasPoint,
getCanvasDropPoint,
setAssetPointerDrag,
setActiveUploadFolderId,
setUploadDropTarget,
setHoveredLayerId,
updateAssetMoveDropFolder,
moveAssetToFolder,
captureCanvasHistory,
appendCanvasLayersWithResources,
selectSingleLayer,
addUploadedFiles,
}: UseImageCanvasAssetCanvasBridgeOptions) {
const addAssetLayer = useCallback(
(asset: EditorAsset, position?: CanvasPoint) => {
setActiveUploadFolderId(asset.folderId);
layerCounterRef.current += 1;
const nextLayer = createLayerFromAsset(
asset,
layerCounterRef.current,
viewport,
{
x: position?.x ?? canvasSize.width / 2,
y: position?.y ?? canvasSize.height / 2,
},
{ applyCascadeOffset: position === undefined },
);
captureCanvasHistory({ type: 'add-image', count: 1 });
appendCanvasLayersWithResources([nextLayer]);
selectSingleLayer(nextLayer.id);
setHoveredLayerId(null);
},
[
appendCanvasLayersWithResources,
canvasSize.height,
canvasSize.width,
captureCanvasHistory,
layerCounterRef,
selectSingleLayer,
setActiveUploadFolderId,
setHoveredLayerId,
viewport,
],
);
useImageCanvasAssetPointerDragBridge({
assetPointerDragRef,
suppressAssetClickRef,
assets,
resolveAssetFolderId,
resolveCanvasPoint,
setAssetPointerDrag,
setUploadDropTarget,
updateAssetMoveDropFolder,
moveAssetToFolder,
addAssetLayer,
});
const canvasDropWorkflow = useImageCanvasCanvasDropWorkflow({
assets,
assetFolders,
setUploadDropTarget,
updateAssetMoveDropFolder,
getCanvasDropPoint,
addAssetLayer,
addUploadedFiles,
});
return useMemo(
() => ({
addAssetLayer,
...canvasDropWorkflow,
}),
[addAssetLayer, canvasDropWorkflow],
);
}