03a4410272
主要工作是共享同一套“画布内核与通用 UI 源码”,网站和 Tauri 只分别实现宿主适配层。 --------- Co-authored-by: 段舒康 <kdletters@qq.com> Co-authored-by: kdletters <kdletters@qq.com> Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/136 Co-authored-by: menghao <mh18530625731@163.com> Co-committed-by: menghao <mh18530625731@163.com>
323 lines
10 KiB
TypeScript
323 lines
10 KiB
TypeScript
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasHistoryAction,
|
|
CanvasHistoryApplyResult,
|
|
CanvasHistoryEntry,
|
|
CanvasHistorySnapshot,
|
|
CanvasLayer,
|
|
CanvasViewport,
|
|
GenerateDialogState,
|
|
} from '@genarrative/image-canvas-core';
|
|
import {
|
|
canRestoreCanvasHistorySnapshotWithoutContentLoss,
|
|
isProtectedCanvasHistoryAction,
|
|
MAX_HISTORY_STEPS,
|
|
mergeCanvasHistorySnapshotForRestore,
|
|
} from '@genarrative/image-canvas-core';
|
|
import { type RefObject, useCallback, useRef, useState } from 'react';
|
|
|
|
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;
|
|
};
|
|
|
|
type CanvasHistoryEffects = {
|
|
persistRestoredAssetKind?: (
|
|
layer: CanvasLayer,
|
|
historyEntryId?: number,
|
|
) => 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,
|
|
effects,
|
|
allowContentRemovalOnRestore = false,
|
|
}: {
|
|
refs: CanvasHistoryRefs;
|
|
setters: CanvasHistorySetters;
|
|
resetters?: CanvasHistoryResetters;
|
|
effects?: CanvasHistoryEffects;
|
|
allowContentRemovalOnRestore?: boolean;
|
|
}) {
|
|
const undoStackRef = useRef<CanvasHistoryEntry[]>([]);
|
|
const redoStackRef = useRef<CanvasHistoryEntry[]>([]);
|
|
const nextHistoryEntryIdRef = useRef(0);
|
|
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 ?? null,
|
|
selectedLayerIds: [...refs.selectedLayerIdsRef.current],
|
|
}),
|
|
[refs],
|
|
);
|
|
|
|
const restoreCanvasHistorySnapshot = useCallback(
|
|
(
|
|
snapshot: CanvasHistorySnapshot,
|
|
action?: CanvasHistoryAction,
|
|
historyEntryId?: number,
|
|
) => {
|
|
const currentSnapshot = getCanvasHistorySnapshot();
|
|
const mergedSnapshot = mergeCanvasHistorySnapshotForRestore({
|
|
current: currentSnapshot,
|
|
target: snapshot,
|
|
assetKindLayerIds:
|
|
action?.type === 'change-asset-kind'
|
|
? action.layerIds?.length
|
|
? new Set(action.layerIds)
|
|
: undefined
|
|
: new Set(),
|
|
layerSizeLayerIds:
|
|
action?.type === 'resize-image'
|
|
? action.layerIds?.length
|
|
? new Set(action.layerIds)
|
|
: new Set(snapshot.layers.map((layer) => layer.id))
|
|
: new Set(),
|
|
});
|
|
setters.setLayers(mergedSnapshot.layers.map((layer) => ({ ...layer })));
|
|
setters.setViewport({ ...mergedSnapshot.viewport });
|
|
setters.setGenerateDialog?.(
|
|
mergedSnapshot.generateDialog
|
|
? cloneGenerateDialog(mergedSnapshot.generateDialog)
|
|
: null,
|
|
);
|
|
setters.setInactiveGenerateDialogs?.(
|
|
mergedSnapshot.inactiveGenerateDialogs.map(cloneCanvasGenerationDialog),
|
|
);
|
|
setters.setSelectedLayerId?.(mergedSnapshot.selectedLayerId);
|
|
setters.setSelectedLayerIds([...mergedSnapshot.selectedLayerIds]);
|
|
resetters?.setHoveredLayerId(null);
|
|
resetters?.setMetadataLayer(null);
|
|
resetters?.resetCanvasInteractionState();
|
|
|
|
if (action?.type === 'change-asset-kind') {
|
|
const currentLayerById = new Map(
|
|
currentSnapshot.layers.map((layer) => [layer.id, layer] as const),
|
|
);
|
|
for (const layer of mergedSnapshot.layers) {
|
|
const currentLayer = currentLayerById.get(layer.id);
|
|
if (currentLayer && currentLayer.assetKind !== layer.assetKind) {
|
|
effects?.persistRestoredAssetKind?.({ ...layer }, historyEntryId);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
[effects, getCanvasHistorySnapshot, resetters, setters],
|
|
);
|
|
|
|
const captureCanvasHistory = useCallback(
|
|
(
|
|
action: CanvasHistoryAction,
|
|
options: { snapshot?: CanvasHistorySnapshot } = {},
|
|
) => {
|
|
nextHistoryEntryIdRef.current += 1;
|
|
const entryId = nextHistoryEntryIdRef.current;
|
|
undoStackRef.current = [
|
|
...undoStackRef.current.slice(-(MAX_HISTORY_STEPS - 1)),
|
|
{
|
|
entryId,
|
|
snapshot: options.snapshot ?? getCanvasHistorySnapshot(),
|
|
action,
|
|
createdAt: Date.now(),
|
|
},
|
|
];
|
|
redoStackRef.current = [];
|
|
setHistoryVersion((version) => version + 1);
|
|
return entryId;
|
|
},
|
|
[getCanvasHistorySnapshot],
|
|
);
|
|
|
|
const discardCanvasHistoryEntriesContainingLayer = useCallback(
|
|
(matchesLayer: (layer: CanvasLayer) => boolean) => {
|
|
const keepEntry = (entry: CanvasHistoryEntry) =>
|
|
!entry.snapshot.layers.some(matchesLayer);
|
|
const nextUndoStack = undoStackRef.current.filter(keepEntry);
|
|
const nextRedoStack = redoStackRef.current.filter(keepEntry);
|
|
if (
|
|
nextUndoStack.length === undoStackRef.current.length &&
|
|
nextRedoStack.length === redoStackRef.current.length
|
|
) {
|
|
return;
|
|
}
|
|
undoStackRef.current = nextUndoStack;
|
|
redoStackRef.current = nextRedoStack;
|
|
setHistoryVersion((version) => version + 1);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const discardCanvasHistoryEntriesById = useCallback(
|
|
(entryIds: ReadonlySet<number>) => {
|
|
if (entryIds.size === 0) {
|
|
return;
|
|
}
|
|
const keepEntry = (entry: CanvasHistoryEntry) =>
|
|
!entryIds.has(entry.entryId);
|
|
const nextUndoStack = undoStackRef.current.filter(keepEntry);
|
|
const nextRedoStack = redoStackRef.current.filter(keepEntry);
|
|
if (
|
|
nextUndoStack.length === undoStackRef.current.length &&
|
|
nextRedoStack.length === redoStackRef.current.length
|
|
) {
|
|
return;
|
|
}
|
|
undoStackRef.current = nextUndoStack;
|
|
redoStackRef.current = nextRedoStack;
|
|
setHistoryVersion((version) => version + 1);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const undoCanvasChange = useCallback((): CanvasHistoryApplyResult => {
|
|
const previousEntry = undoStackRef.current.at(-1);
|
|
if (!previousEntry) {
|
|
return { status: 'empty' };
|
|
}
|
|
const currentSnapshot = getCanvasHistorySnapshot();
|
|
if (
|
|
!allowContentRemovalOnRestore &&
|
|
(isProtectedCanvasHistoryAction(previousEntry.action) ||
|
|
!canRestoreCanvasHistorySnapshotWithoutContentLoss({
|
|
current: currentSnapshot,
|
|
target: previousEntry.snapshot,
|
|
}))
|
|
) {
|
|
return {
|
|
status: 'blocked',
|
|
action: previousEntry.action,
|
|
reason: 'content-may-disappear',
|
|
};
|
|
}
|
|
undoStackRef.current = undoStackRef.current.slice(0, -1);
|
|
redoStackRef.current = [
|
|
...redoStackRef.current.slice(-(MAX_HISTORY_STEPS - 1)),
|
|
{
|
|
entryId: previousEntry.entryId,
|
|
snapshot: currentSnapshot,
|
|
action: previousEntry.action,
|
|
createdAt: Date.now(),
|
|
},
|
|
];
|
|
restoreCanvasHistorySnapshot(
|
|
previousEntry.snapshot,
|
|
previousEntry.action,
|
|
previousEntry.entryId,
|
|
);
|
|
setHistoryVersion((version) => version + 1);
|
|
return { status: 'success', action: previousEntry.action };
|
|
}, [
|
|
allowContentRemovalOnRestore,
|
|
getCanvasHistorySnapshot,
|
|
restoreCanvasHistorySnapshot,
|
|
]);
|
|
|
|
const redoCanvasChange = useCallback((): CanvasHistoryApplyResult => {
|
|
const nextEntry = redoStackRef.current.at(-1);
|
|
if (!nextEntry) {
|
|
return { status: 'empty' };
|
|
}
|
|
const currentSnapshot = getCanvasHistorySnapshot();
|
|
if (
|
|
!allowContentRemovalOnRestore &&
|
|
!canRestoreCanvasHistorySnapshotWithoutContentLoss({
|
|
current: currentSnapshot,
|
|
target: nextEntry.snapshot,
|
|
})
|
|
) {
|
|
return {
|
|
status: 'blocked',
|
|
action: nextEntry.action,
|
|
reason: 'content-may-disappear',
|
|
};
|
|
}
|
|
redoStackRef.current = redoStackRef.current.slice(0, -1);
|
|
undoStackRef.current = [
|
|
...undoStackRef.current.slice(-(MAX_HISTORY_STEPS - 1)),
|
|
{
|
|
entryId: nextEntry.entryId,
|
|
snapshot: currentSnapshot,
|
|
action: nextEntry.action,
|
|
createdAt: Date.now(),
|
|
},
|
|
];
|
|
restoreCanvasHistorySnapshot(
|
|
nextEntry.snapshot,
|
|
nextEntry.action,
|
|
nextEntry.entryId,
|
|
);
|
|
setHistoryVersion((version) => version + 1);
|
|
return { status: 'success', action: nextEntry.action };
|
|
}, [
|
|
allowContentRemovalOnRestore,
|
|
getCanvasHistorySnapshot,
|
|
restoreCanvasHistorySnapshot,
|
|
]);
|
|
|
|
const resetCanvasHistory = useCallback(() => {
|
|
undoStackRef.current = [];
|
|
redoStackRef.current = [];
|
|
setHistoryVersion((version) => version + 1);
|
|
}, []);
|
|
|
|
return {
|
|
canUndo: undoStackRef.current.length > 0,
|
|
canRedo: redoStackRef.current.length > 0,
|
|
historyVersion,
|
|
getCanvasHistorySnapshot,
|
|
restoreCanvasHistorySnapshot,
|
|
captureCanvasHistory,
|
|
discardCanvasHistoryEntriesContainingLayer,
|
|
discardCanvasHistoryEntriesById,
|
|
undoCanvasChange,
|
|
redoCanvasChange,
|
|
resetCanvasHistory,
|
|
};
|
|
}
|