Files
Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts
T
kdletters b66832396b 修复图片画布撤销恢复安全边界
保留图层资源关联和生成对象当前任务与尺寸,仅恢复安全布局状态
按目标槽位恢复生成面板开关并按稳定媒体标识判断内容变化
补齐本地生成结果历史并避免自动视图覆盖栈顶
阻止阈值内指针抖动产生不可撤销位移
补充异步回填、2K尺寸和跨槽位恢复测试并同步文档与踩坑记录
2026-07-18 18:19:38 +08:00

2661 lines
82 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
type Dispatch,
type MutableRefObject,
type PointerEvent as ReactPointerEvent,
type SetStateAction,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
import { uploadEditorMediaAssetFile } from '../../services/image-editor/editorMediaAssetUploadClient';
import {
createEditorProjectResource,
type EditorAssetSnapshot,
type EditorProjectLayerSnapshot,
type EditorProjectResourceSnapshot,
type EditorProjectSnapshot,
splitEditorIconSpritesheet,
} from '../../services/image-editor/editorProjectClient';
import { resizeCropExpandFrame } from './ImageCanvasCropExpandModel';
import type {
CanvasGenerationDialogState,
CanvasHistoryAction,
CanvasLayer,
CanvasTool,
CanvasViewport,
CharacterAnimationPanelState,
CharacterReferenceImage,
CropExpandPanelState,
CropExpandResizeHandle,
GenerateDialogState,
ImageContextMenuState,
PublicationMaterialsWorkflowId,
QuickEditPanelState,
SidebarPanel,
SpecFormValues,
SpecGenerationType,
} from './ImageCanvasEditorTypes';
import {
appendCharacterReference,
appendGenerationReference,
appendPublicationReference,
assignCharacterSpecReference,
assignIconSpecReference,
assignUiDesignSpecReference,
closeGenerateComposerDialog,
createAudioRedrawGenerationDialogDraft,
createBackgroundMusicGenerationDialogDraft,
createCharacterAnimationGenerationDialogDraft,
createCharacterGenerationDialogDraft,
createEditDialogDraft,
createGenerateDialogDraft,
createIconGenerationDialogDraft,
createPublicationGenerationDialogDraft,
createQuickEditGenerationDialogDraft,
createQuickEditPanelDraft,
createRedrawPanelDraft,
createSameSourceGenerationDialogDraft,
createSoundEffectGenerationDialogDraft,
createSpecDialogDraft,
createUiDesignGenerationDialogDraft,
createVideoGenerationDialogDraft,
createVideoRedrawGenerationDialogDraft,
hideGeneratedLayerComposerAfterBlur,
isCharacterSpecReferenceLayer,
isIconSpecReferenceLayer,
updateCharacterAnimationDurationPanel,
updateIconDescriptionsTextInDialog,
updateSpecFormDialogValue,
} from './ImageCanvasGenerationDialogModel';
import {
calculateCharacterAnimationPrice,
CHARACTER_ANIMATION_DURATION_OPTIONS,
CHARACTER_ANIMATION_MODEL,
DEFAULT_ICON_DESCRIPTIONS,
DEFAULT_IMAGE_MODEL,
EDITOR_IMAGE_DIMENSION_OPTIONS,
ICON_DESCRIPTION_LIMIT,
IMAGE_MODEL_GPT_IMAGE_2,
isCanvasGenerationDialog,
isQuickEditUnsupportedAssetKind,
normalizeEditorImageModel,
resizeGenerationPlaceholderToImageSelection,
resolveEditorImageGenerationPixelSize,
} from './ImageCanvasGenerationModel';
import {
centerViewportOnPlacement,
chooseGenerationPlacement,
} from './ImageCanvasGenerationPlacementModel';
import { resolveQuickEditFocusViewport } from './ImageCanvasOverlayModel';
import {
buildCropExpandInsetsFromFrame,
removeImageBackground,
renderCropExpandImage,
} from './ImageCanvasRasterEditModel';
import {
createUiAssetExtractionDraftMark,
createUiAssetExtractionState,
normalizeUiAssetExtractionMark,
type UiAssetExtractionState,
type UiAssetExtractionTool,
updateUiAssetExtractionDraftMark,
} from './ImageCanvasUiAssetExtractionModel';
import {
applyQueuedEditorGenerationProject,
createEditorGenerationMediaUploadId,
resolveEditorGenerationMediaReference,
useImageCanvasGenerationSubmissionWorkflow,
} from './useImageCanvasGenerationSubmissionWorkflow';
type CanvasSize = { width: number; height: number };
type CropExpandResizeDragState = {
pointerId: number;
handle: CropExpandResizeHandle;
sourceLayerId: string;
startClientX: number;
startClientY: number;
startScale: number;
startFrame: CropExpandPanelState['frame'];
ratio: CropExpandPanelState['ratio'];
};
type CanvasGenerationDialogUpdater = (
dialog: CanvasGenerationDialogState,
) => CanvasGenerationDialogState | null;
type RememberedImageGenerationOptions = {
model: string;
aspectRatio: string;
imageSize: string;
};
function dataUrlToImageFile(dataUrl: string, fileName: string) {
const [header = '', payload = ''] = dataUrl.split(',');
const mimeMatch = /^data:([^;]+)(;base64)?$/iu.exec(header);
const type = mimeMatch?.[1] ?? 'image/png';
const isBase64 = Boolean(mimeMatch?.[2]);
const binary = isBase64
? typeof atob === 'function'
? atob(payload)
: Buffer.from(payload, 'base64').toString('binary')
: decodeURIComponent(payload);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return new File([bytes], fileName, { type });
}
function createProjectResourceSnapshotFromLayer(
projectId: string,
layer: CanvasLayer,
): EditorProjectResourceSnapshot | null {
const imageSrc = layer.objectKey?.trim()
? `/${layer.objectKey.trim().replace(/^\/+/u, '')}`
: layer.src.trim();
if (!imageSrc) {
return null;
}
return {
resourceId: layer.resourceId,
projectId,
label: layer.title,
imageSrc,
objectKey: layer.objectKey,
assetObjectId: layer.assetObjectId,
width: layer.originalWidth,
height: layer.originalHeight,
sourceType: layer.sourceType,
prompt: layer.prompt,
actualPrompt: layer.actualPrompt,
model: layer.model,
provider: layer.provider,
taskId: layer.taskId,
durationSeconds: layer.durationSeconds,
sourceResourceId: layer.sourceResourceId,
assetKind: layer.assetKind,
generationInputs: layer.generationInputs,
};
}
function createProjectLayerSnapshotFromLayer(
layer: CanvasLayer,
): EditorProjectLayerSnapshot {
return {
layerId: layer.id,
resourceId: layer.resourceId,
title: layer.title,
x: layer.x,
y: layer.y,
width: layer.width,
height: layer.height,
originalWidth: layer.originalWidth,
originalHeight: layer.originalHeight,
zIndex: layer.zIndex,
sourceType: layer.sourceType,
sourceResourceId: layer.sourceResourceId,
assetKind: layer.assetKind,
mediaType: layer.mediaType,
objectKey: layer.objectKey,
assetObjectId: layer.assetObjectId,
durationSeconds: layer.durationSeconds,
};
}
function preserveSourceLayerInProjectSnapshot(
project: EditorProjectSnapshot,
sourceLayer: CanvasLayer,
): EditorProjectSnapshot {
if (
project.layers.some((layer) => layer.layerId === sourceLayer.id) ||
sourceLayer.resourceId.startsWith('generation-dialog:')
) {
return project;
}
const sourceResource = project.resources.some(
(resource) => resource.resourceId === sourceLayer.resourceId,
)
? null
: createProjectResourceSnapshotFromLayer(project.projectId, sourceLayer);
return {
...project,
resources: sourceResource
? [...project.resources, sourceResource]
: project.resources,
layers: [
createProjectLayerSnapshotFromLayer(sourceLayer),
...project.layers,
],
};
}
function findSourceGenerationDialog(
dialogs: CanvasGenerationDialogState[],
sourceLayer: CanvasLayer,
) {
return [...dialogs]
.reverse()
.find(
(dialog) =>
dialog.generatedLayerId === sourceLayer.id ||
// sourceLayerId 也用于派生生成器的输入关系,类型不匹配时不能当作图层来源。
(!dialog.generatedLayerId &&
dialog.sourceLayerId === sourceLayer.id &&
isGenerationDialogModeCompatibleWithSourceLayer(
dialog.mode,
sourceLayer,
)),
);
}
function isGenerationDialogModeCompatibleWithSourceLayer(
mode: CanvasGenerationDialogState['mode'],
sourceLayer: CanvasLayer,
) {
if (mode === 'quick-edit') {
return false;
}
if (sourceLayer.assetKind === 'character') {
return mode === 'character';
}
if (sourceLayer.assetKind === 'character-animation') {
return mode === 'character-animation';
}
if (sourceLayer.assetKind === 'ui-design') {
return mode === 'ui-design';
}
if (sourceLayer.assetKind === 'publication-material') {
return mode === 'publication';
}
if (
sourceLayer.assetKind === 'icon' ||
sourceLayer.assetKind === 'icon-spritesheet'
) {
return mode === 'icon';
}
if (
sourceLayer.assetKind === 'spec' ||
sourceLayer.assetKind === 'icon-spec'
) {
return mode === 'spec';
}
if (sourceLayer.assetKind === 'video') {
return mode === 'video';
}
if (sourceLayer.assetKind === 'sound-effect') {
return mode === 'audio-sound-effect';
}
if (sourceLayer.assetKind === 'background-music') {
return mode === 'audio-background-music';
}
return true;
}
function findSourceAnimationLayer(
layers: CanvasLayer[],
sourceLayer: CanvasLayer,
) {
if (sourceLayer.assetKind !== 'character-animation') {
return null;
}
const sourceResourceId = sourceLayer.sourceResourceId?.trim();
if (!sourceResourceId) {
return null;
}
return (
layers.find(
(layer) =>
layer.resourceId === sourceResourceId &&
layer.assetKind === 'character',
) ?? null
);
}
function getCanvasToolForGenerationMode(
mode: CanvasGenerationDialogState['mode'],
): CanvasTool {
if (mode === 'video') {
return 'video';
}
if (mode === 'audio-sound-effect' || mode === 'audio-background-music') {
return 'music';
}
if (mode === 'character' || mode === 'character-animation') {
return 'character';
}
if (mode === 'icon') {
return 'icon';
}
if (mode === 'publication') {
return 'publication';
}
if (mode === 'ui-design') {
return 'ui-design';
}
if (mode === 'spec') {
return 'spec';
}
return 'generate';
}
const LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY =
'genarrative.imageCanvas.lastCharacterSpecReference';
const LAST_ICON_SPEC_REFERENCE_CACHE_KEY =
'genarrative.imageCanvas.lastIconSpecReference';
const INVALID_CHARACTER_SPEC_WARNING =
'选择的图片不是角色规范图,请选择生成规范里的角色规范。';
const INVALID_ICON_SPEC_WARNING =
'选择的图片不是图标规范图,请选择生成规范里的图标规范。';
function normalizeRememberedImageGenerationOptions({
model,
aspectRatio,
imageSize,
}: Partial<RememberedImageGenerationOptions>): RememberedImageGenerationOptions {
const normalizedModel = normalizeEditorImageModel(model);
const dimensionOptions =
EDITOR_IMAGE_DIMENSION_OPTIONS[
normalizedModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
const aspectRatios = dimensionOptions.aspectRatios as readonly string[];
const imageSizes = dimensionOptions.imageSizes as readonly string[];
return {
model: normalizedModel,
aspectRatio:
aspectRatio && aspectRatios.includes(aspectRatio)
? aspectRatio
: (dimensionOptions.aspectRatios[0] ?? '1:1'),
imageSize:
imageSize && imageSizes.includes(imageSize)
? imageSize
: (dimensionOptions.imageSizes.find((size) => size === '1K') ??
dimensionOptions.imageSizes[0] ??
'1K'),
};
}
function applyRememberedImageGenerationOptions(
draft: Omit<CanvasGenerationDialogState, 'id'>,
options: RememberedImageGenerationOptions,
): Omit<CanvasGenerationDialogState, 'id'> {
return resizeGenerationPlaceholderToImageSelection({
...draft,
...normalizeRememberedImageGenerationOptions(options),
}) as Omit<CanvasGenerationDialogState, 'id'>;
}
function shouldRememberImageGenerationOptions(
dialog: GenerateDialogState | null,
): dialog is GenerateDialogState & {
mode: 'generate' | 'character' | 'icon';
} {
return (
dialog?.mode === 'generate' ||
dialog?.mode === 'character' ||
dialog?.mode === 'icon'
);
}
function appendQuickEditSelectionPrompt(prompt: string, markNumber: number) {
const nextLine = `${markNumber}号红色圈选框里的内容做以下修改:`;
const trimmedPrompt = prompt.trimEnd();
return trimmedPrompt ? `${trimmedPrompt}\n${nextLine}` : nextLine;
}
function getLocalGenerationReferenceStorage() {
if (typeof window === 'undefined') {
return null;
}
try {
return window.localStorage;
} catch {
return null;
}
}
function readOptionalCachedString(
record: Record<string, unknown>,
key: keyof CharacterReferenceImage,
) {
const value = record[key];
return typeof value === 'string' ? value : undefined;
}
function readOptionalCachedNullableString(
record: Record<string, unknown>,
key: keyof CharacterReferenceImage,
) {
const value = record[key];
if (value === null) {
return null;
}
return typeof value === 'string' ? value : undefined;
}
function readCachedGenerationReference(
key: string,
currentUserId?: string | null,
): CharacterReferenceImage | null {
const storage = getLocalGenerationReferenceStorage();
if (!storage) {
return null;
}
try {
const rawValue = storage.getItem(key);
if (!rawValue) {
return null;
}
const parsedValue: unknown = JSON.parse(rawValue);
if (!parsedValue || typeof parsedValue !== 'object') {
return null;
}
const record = parsedValue as Record<string, unknown>;
const normalizedCurrentUserId = currentUserId?.trim();
const ownerUserId =
typeof record.ownerUserId === 'string' ? record.ownerUserId.trim() : '';
if (normalizedCurrentUserId && ownerUserId !== normalizedCurrentUserId) {
storage.removeItem(key);
return null;
}
const id = readOptionalCachedString(record, 'id');
const label = readOptionalCachedString(record, 'label');
const src = readOptionalCachedString(record, 'src');
if (!id || !label || !src) {
return null;
}
const reference: CharacterReferenceImage = { id, label, src };
const mediaType = readOptionalCachedString(record, 'mediaType');
if (
mediaType === 'image' ||
mediaType === 'video' ||
mediaType === 'audio'
) {
reference.mediaType = mediaType;
}
const mimeType = readOptionalCachedString(record, 'mimeType');
if (mimeType) {
reference.mimeType = mimeType;
}
const sizeBytes = record.sizeBytes;
if (typeof sizeBytes === 'number') {
reference.sizeBytes = sizeBytes;
}
const durationSeconds = record.durationSeconds;
if (typeof durationSeconds === 'number') {
reference.durationSeconds = durationSeconds;
}
for (const keyName of [
'objectKey',
'assetObjectId',
'resourceId',
'sourceAssetId',
] as const) {
const value = readOptionalCachedNullableString(record, keyName);
if (value !== undefined) {
reference[keyName] = value;
}
}
return reference;
} catch {
return null;
}
}
function writeCachedGenerationReference(
key: string,
reference: CharacterReferenceImage,
currentUserId?: string | null,
) {
const storage = getLocalGenerationReferenceStorage();
if (!storage) {
return;
}
try {
const ownerUserId = currentUserId?.trim();
storage.setItem(
key,
JSON.stringify(ownerUserId ? { ...reference, ownerUserId } : reference),
);
} catch {
// 中文注释:本地缓存只提升下次新建素材的便捷性,写入失败不阻断生成流程。
}
}
function createCharacterAnimationPanelFromDialog(
dialog: CanvasGenerationDialogState | null,
): CharacterAnimationPanelState | null {
if (dialog?.mode !== 'character-animation' || !dialog.sourceLayerId) {
return null;
}
return {
sourceLayerId: dialog.sourceLayerId,
promptText: dialog.prompt,
assetLabel: dialog.assetLabel,
resolution: dialog.characterAnimationResolution ?? '480p',
ratio: dialog.characterAnimationRatio ?? 'same',
frameCount: dialog.characterAnimationFrameCount ?? 32,
durationSeconds: dialog.characterAnimationDurationSeconds ?? 4,
status:
dialog.status === 'generating'
? 'generating'
: dialog.status === 'failed'
? 'failed'
: dialog.characterAnimationResult
? 'completed'
: 'idle',
errorMessage: dialog.errorMessage,
result: dialog.characterAnimationResult,
};
}
function applyCharacterAnimationPanelToDialog(
dialog: CanvasGenerationDialogState,
panel: CharacterAnimationPanelState | null,
): CanvasGenerationDialogState {
if (dialog.mode !== 'character-animation') {
return dialog;
}
if (!panel) {
return {
...dialog,
composerOpen: false,
};
}
return {
...dialog,
prompt: panel.promptText,
assetLabel: panel.assetLabel,
characterAnimationResolution: panel.resolution,
characterAnimationRatio: panel.ratio,
characterAnimationFrameCount: panel.frameCount,
characterAnimationDurationSeconds: panel.durationSeconds,
characterAnimationResult: panel.result,
status:
panel.status === 'generating'
? 'generating'
: panel.status === 'failed'
? 'failed'
: 'idle',
errorMessage: panel.errorMessage,
};
}
type GenerationWorkflowOptions = {
layers: CanvasLayer[];
canvasSize: CanvasSize;
viewport: CanvasViewport;
setViewport: Dispatch<SetStateAction<CanvasViewport>>;
setLayers: Dispatch<SetStateAction<CanvasLayer[]>>;
canvasGenerationDialogs: CanvasGenerationDialogState[];
layerCounterRef: MutableRefObject<number>;
generateDialog: GenerateDialogState | null;
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
openCanvasGenerationDialog: (
dialog: Omit<CanvasGenerationDialogState, 'id'>,
) => string;
updateCanvasGenerationDialogById: (
dialogId: string,
updater: CanvasGenerationDialogUpdater,
) => void;
hasCanvasGenerationDialogById: (dialogId: string) => boolean;
archiveActiveCanvasGenerationDialog: () => void;
removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void;
getGeneratingDialogPlaceholder: (
dialog: GenerateDialogState,
) => GenerateDialogState['placeholder'];
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
selectSingleLayer: (layerId: string | null) => void;
fitLayers: (
targetLayers?: CanvasLayer[],
options?: { captureHistory?: boolean },
) => void;
captureCanvasHistory: (action: CanvasHistoryAction) => void;
setActiveTool: Dispatch<SetStateAction<CanvasTool>>;
setActiveSidebarPanel: Dispatch<SetStateAction<SidebarPanel | null>>;
setMetadataLayer: Dispatch<SetStateAction<CanvasLayer | null>>;
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
persistGeneratedAsset?: (layer: CanvasLayer) => void;
persistUpdatedLayerResource?: (layer: CanvasLayer) => void;
projectId?: string | null;
currentUserId?: string | null;
assetFolderId?: string | null;
upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void;
applyProjectSnapshot?: (
project: EditorProjectSnapshot,
action?: CanvasHistoryAction,
) => void;
onWalletBalanceMayHaveChanged?: () => void;
};
export function useImageCanvasGenerationWorkflow({
layers,
canvasSize,
viewport,
setViewport,
setLayers,
canvasGenerationDialogs,
layerCounterRef,
generateDialog,
setGenerateDialog,
openCanvasGenerationDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
archiveActiveCanvasGenerationDialog,
removeCanvasGenerationDialogsByLayerId,
getGeneratingDialogPlaceholder,
appendCanvasLayersWithResources,
selectSingleLayer,
fitLayers,
captureCanvasHistory,
setActiveTool,
setActiveSidebarPanel,
setMetadataLayer,
setImageContextMenu,
persistGeneratedAsset,
persistUpdatedLayerResource,
projectId,
currentUserId,
assetFolderId,
upsertGeneratedAsset,
applyProjectSnapshot,
onWalletBalanceMayHaveChanged,
}: GenerationWorkflowOptions) {
const [isTaskSidebarOpen, setIsTaskSidebarOpen] = useState(false);
const [taskListRefreshKey, setTaskListRefreshKey] = useState(0);
const refreshTaskListForQueuedGeneration = useCallback(() => {
setIsTaskSidebarOpen(true);
setTaskListRefreshKey((key) => key + 1);
}, []);
const refreshTaskList = useCallback(() => {
setTaskListRefreshKey((key) => key + 1);
}, []);
const previousTaskCountRef = useRef(canvasGenerationDialogs.length);
const splittingIconSpritesheetLayerIdsRef = useRef(new Set<string>());
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
const [isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen] =
useState(false);
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(false);
const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] =
useState(false);
const [
isPickingGenerationReferenceFromCanvas,
setIsPickingGenerationReferenceFromCanvas,
] = useState(false);
const [
isPickingQuickEditReferenceFromCanvas,
setIsPickingQuickEditReferenceFromCanvas,
] = useState(false);
const [
isPickingCharacterSpecFromCanvas,
setIsPickingCharacterSpecFromCanvas,
] = useState(false);
const [
isPickingCharacterReferenceFromCanvas,
setIsPickingCharacterReferenceFromCanvas,
] = useState(false);
const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(false);
const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
useState(false);
const [isUiDesignSpecMenuOpen, setIsUiDesignSpecMenuOpen] = useState(false);
const [isPickingUiDesignSpecFromCanvas, setIsPickingUiDesignSpecFromCanvas] =
useState(false);
const [isMusicMenuOpen, setIsMusicMenuOpen] = useState(false);
const [isPublicationMenuOpen, setIsPublicationMenuOpen] = useState(false);
const [isPublicationReferenceMenuOpen, setIsPublicationReferenceMenuOpen] =
useState(false);
const [
isPickingPublicationReferenceFromCanvas,
setIsPickingPublicationReferenceFromCanvas,
] = useState(false);
const [quickEditPanel, setQuickEditPanel] =
useState<QuickEditPanelState | null>(null);
const [generationWarning, setGenerationWarning] = useState<string | null>(
null,
);
const [cropExpandPanel, setCropExpandPanel] =
useState<CropExpandPanelState | null>(null);
const [uiAssetExtractionState, setUiAssetExtractionState] =
useState<UiAssetExtractionState | null>(null);
const [quickEditSelectionState, setQuickEditSelectionState] =
useState<UiAssetExtractionState | null>(null);
const quickEditSelectionStateRef = useRef<UiAssetExtractionState | null>(
null,
);
const cropExpandPanelRef = useRef<CropExpandPanelState | null>(null);
const cropExpandResizeDragRef = useRef<CropExpandResizeDragState | null>(
null,
);
const uiAssetExtractionMarkCounterRef = useRef(0);
useEffect(() => {
if (canvasGenerationDialogs.length > previousTaskCountRef.current) {
setIsTaskSidebarOpen(true);
}
previousTaskCountRef.current = canvasGenerationDialogs.length;
}, [canvasGenerationDialogs.length]);
const quickEditSelectionMarkCounterRef = useRef(0);
const [characterAnimationPanel, setCharacterAnimationPanel] =
useState<CharacterAnimationPanelState | null>(null);
const [rememberedImageOptions, setRememberedImageOptions] =
useState<RememberedImageGenerationOptions>(() =>
normalizeRememberedImageGenerationOptions({ model: DEFAULT_IMAGE_MODEL }),
);
const [initialCharacterSpecReference] =
useState<CharacterReferenceImage | null>(() =>
readCachedGenerationReference(
LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY,
currentUserId,
),
);
const [initialIconSpecReference] = useState<CharacterReferenceImage | null>(
() =>
readCachedGenerationReference(
LAST_ICON_SPEC_REFERENCE_CACHE_KEY,
currentUserId,
),
);
const currentUserIdRef = useRef(currentUserId);
const lastCharacterSpecReferenceRef = useRef<CharacterReferenceImage | null>(
initialCharacterSpecReference,
);
const lastIconSpecReferenceRef = useRef<CharacterReferenceImage | null>(
initialIconSpecReference,
);
const characterAnimationDialog =
isCanvasGenerationDialog(generateDialog) &&
generateDialog.mode === 'character-animation'
? generateDialog
: null;
const characterAnimationDialogPanel = createCharacterAnimationPanelFromDialog(
characterAnimationDialog,
);
const effectiveCharacterAnimationPanel =
characterAnimationDialogPanel ?? characterAnimationPanel;
cropExpandPanelRef.current = cropExpandPanel;
quickEditSelectionStateRef.current = quickEditSelectionState;
useEffect(() => {
currentUserIdRef.current = currentUserId;
lastCharacterSpecReferenceRef.current = readCachedGenerationReference(
LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY,
currentUserId,
);
lastIconSpecReferenceRef.current = readCachedGenerationReference(
LAST_ICON_SPEC_REFERENCE_CACHE_KEY,
currentUserId,
);
}, [currentUserId]);
const updateQuickEditSelectionState = useCallback(
(
updater: (
currentState: UiAssetExtractionState | null,
) => UiAssetExtractionState | null,
) => {
const nextState = updater(quickEditSelectionStateRef.current);
quickEditSelectionStateRef.current = nextState;
setQuickEditSelectionState(nextState);
return nextState;
},
[],
);
useEffect(() => {
if (
generateDialog?.mode === 'character' &&
generateDialog.characterSpecReference
) {
lastCharacterSpecReferenceRef.current =
generateDialog.characterSpecReference;
writeCachedGenerationReference(
LAST_CHARACTER_SPEC_REFERENCE_CACHE_KEY,
generateDialog.characterSpecReference,
currentUserIdRef.current,
);
return;
}
if (generateDialog?.mode === 'icon' && generateDialog.iconSpecReference) {
lastIconSpecReferenceRef.current = generateDialog.iconSpecReference;
writeCachedGenerationReference(
LAST_ICON_SPEC_REFERENCE_CACHE_KEY,
generateDialog.iconSpecReference,
currentUserIdRef.current,
);
return;
}
if (
generateDialog?.mode === 'ui-design' &&
generateDialog.uiDesignSpecReference
) {
lastIconSpecReferenceRef.current = generateDialog.uiDesignSpecReference;
writeCachedGenerationReference(
LAST_ICON_SPEC_REFERENCE_CACHE_KEY,
generateDialog.uiDesignSpecReference,
currentUserIdRef.current,
);
}
}, [generateDialog]);
useEffect(() => {
if (!shouldRememberImageGenerationOptions(generateDialog)) {
return;
}
const nextOptions = normalizeRememberedImageGenerationOptions({
model: generateDialog.imageModel,
aspectRatio: generateDialog.aspectRatio,
imageSize: generateDialog.imageSize,
});
setRememberedImageOptions((currentOptions) =>
currentOptions.model === nextOptions.model &&
currentOptions.aspectRatio === nextOptions.aspectRatio &&
currentOptions.imageSize === nextOptions.imageSize
? currentOptions
: nextOptions,
);
}, [generateDialog]);
const quickEditSourceLayer = quickEditPanel
? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ??
null)
: null;
const cropExpandSourceLayer = cropExpandPanel
? (layers.find((layer) => layer.id === cropExpandPanel.sourceLayerId) ??
null)
: null;
const uiAssetExtractionSourceLayer = uiAssetExtractionState
? (layers.find(
(layer) => layer.id === uiAssetExtractionState.sourceLayerId,
) ?? null)
: null;
const quickEditSelectionSourceLayer =
quickEditPanel?.status !== 'generating' && quickEditSelectionState
? (layers.find(
(layer) => layer.id === quickEditSelectionState.sourceLayerId,
) ?? null)
: null;
const characterAnimationSourceLayer = effectiveCharacterAnimationPanel
? (layers.find(
(layer) => layer.id === effectiveCharacterAnimationPanel.sourceLayerId,
) ?? null)
: null;
const characterAnimationPrice = effectiveCharacterAnimationPanel
? calculateCharacterAnimationPrice(
CHARACTER_ANIMATION_MODEL,
effectiveCharacterAnimationPanel.resolution,
effectiveCharacterAnimationPanel.durationSeconds,
)
: 0;
const iconDescriptionValues =
generateDialog?.mode === 'icon'
? generateDialog.prompt.trim()
? generateDialog.prompt
.split(/[\r\n,,、;/|]+/u)
.map((description) => description.trim())
.filter(Boolean)
.slice(0, ICON_DESCRIPTION_LIMIT)
: (generateDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS)
: DEFAULT_ICON_DESCRIPTIONS;
const closeGenerationTransientState = useCallback(() => {
setIsSpecMenuOpen(false);
setIsGenerationReferenceMenuOpen(false);
setIsPickingQuickEditReferenceFromCanvas(false);
setIsCharacterSpecMenuOpen(false);
setIsCharacterReferenceMenuOpen(false);
setIsPickingGenerationReferenceFromCanvas(false);
setIsPickingCharacterSpecFromCanvas(false);
setIsPickingCharacterReferenceFromCanvas(false);
setIsIconSpecMenuOpen(false);
setIsPickingIconSpecFromCanvas(false);
setIsUiDesignSpecMenuOpen(false);
setIsPickingUiDesignSpecFromCanvas(false);
setIsMusicMenuOpen(false);
setIsPublicationMenuOpen(false);
setIsPublicationReferenceMenuOpen(false);
setIsPickingPublicationReferenceFromCanvas(false);
setUiAssetExtractionState(null);
setQuickEditSelectionState(null);
setImageContextMenu(null);
}, [setImageContextMenu]);
const openPlacedCanvasGenerationDialog = useCallback(
(draft: Omit<CanvasGenerationDialogState, 'id'>) => {
const draftPlaceholder = draft.placeholder;
if (!draftPlaceholder) {
return {
dialogId: openCanvasGenerationDialog(draft),
placeholder: undefined,
};
}
// 中文注释:所有画布生成入口统一先走 placement 模型,避免新占位压住已有图层或生成占位。
const placement = chooseGenerationPlacement({
canvasSize,
viewport,
frame: draftPlaceholder,
layers,
generationDialogs: canvasGenerationDialogs,
});
const dialogId = openCanvasGenerationDialog({
...draft,
placeholder: placement,
});
setViewport(
centerViewportOnPlacement({
canvasSize,
viewport,
placement,
}),
);
return { dialogId, placeholder: placement };
},
[
canvasGenerationDialogs,
canvasSize,
layers,
openCanvasGenerationDialog,
setViewport,
viewport,
],
);
const activateCanvasGenerationEntry = useCallback(
(activeTool: CanvasTool) => {
closeGenerationTransientState();
setActiveTool(activeTool);
selectSingleLayer(null);
setQuickEditPanel(null);
setCropExpandPanel(null);
setCharacterAnimationPanel(null);
setUiAssetExtractionState(null);
setQuickEditSelectionState(null);
},
[closeGenerationTransientState, selectSingleLayer, setActiveTool],
);
const openGenerateDialog = useCallback(() => {
openPlacedCanvasGenerationDialog(
applyRememberedImageGenerationOptions(
createGenerateDialogDraft({ canvasSize, viewport }),
rememberedImageOptions,
),
);
activateCanvasGenerationEntry('generate');
}, [
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
rememberedImageOptions,
viewport,
]);
const openSpecDialog = useCallback(
(specType: SpecGenerationType) => {
openPlacedCanvasGenerationDialog(
createSpecDialogDraft({ canvasSize, viewport, specType }),
);
activateCanvasGenerationEntry('generate');
},
[
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
viewport,
],
);
const openCharacterAnimationPanel = useCallback(
(layer: CanvasLayer) => {
const draft = createCharacterAnimationGenerationDialogDraft({
canvasSize,
viewport,
layer,
});
if (!draft) {
return;
}
openPlacedCanvasGenerationDialog(draft);
activateCanvasGenerationEntry('character');
},
[
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
viewport,
],
);
const openCharacterGenerationDialog = useCallback(() => {
const draft = applyRememberedImageGenerationOptions(
createCharacterGenerationDialogDraft({
canvasSize,
viewport,
imageModel: rememberedImageOptions.model,
}),
rememberedImageOptions,
);
openPlacedCanvasGenerationDialog({
...draft,
characterSpecReference:
lastCharacterSpecReferenceRef.current ?? draft.characterSpecReference,
});
activateCanvasGenerationEntry('character');
}, [
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
rememberedImageOptions,
viewport,
]);
const openIconGenerationDialog = useCallback(() => {
const draft = applyRememberedImageGenerationOptions(
createIconGenerationDialogDraft({
canvasSize,
viewport,
imageModel: rememberedImageOptions.model,
}),
rememberedImageOptions,
);
openPlacedCanvasGenerationDialog({
...draft,
iconSpecReference:
lastIconSpecReferenceRef.current ?? draft.iconSpecReference,
});
activateCanvasGenerationEntry('icon');
}, [
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
rememberedImageOptions,
viewport,
]);
const openPublicationGenerationDialog = useCallback(
(workflowId: PublicationMaterialsWorkflowId) => {
openPlacedCanvasGenerationDialog(
createPublicationGenerationDialogDraft({
canvasSize,
viewport,
workflowId,
}),
);
activateCanvasGenerationEntry('publication');
},
[
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
viewport,
],
);
const openVideoGenerationDialog = useCallback(() => {
openPlacedCanvasGenerationDialog(
createVideoGenerationDialogDraft({ canvasSize, viewport }),
);
activateCanvasGenerationEntry('video');
}, [
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
viewport,
]);
const openUiDesignGenerationDialog = useCallback(() => {
const draft = createUiDesignGenerationDialogDraft({
canvasSize,
viewport,
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
});
openPlacedCanvasGenerationDialog({
...draft,
uiDesignSpecReference:
lastIconSpecReferenceRef.current ?? draft.uiDesignSpecReference,
});
activateCanvasGenerationEntry('ui-design');
}, [
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
viewport,
]);
const openSoundEffectGenerationDialog = useCallback(() => {
openPlacedCanvasGenerationDialog(
createSoundEffectGenerationDialogDraft({ canvasSize, viewport }),
);
activateCanvasGenerationEntry('music');
}, [
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
viewport,
]);
const openBackgroundMusicGenerationDialog = useCallback(() => {
openPlacedCanvasGenerationDialog(
createBackgroundMusicGenerationDialogDraft({ canvasSize, viewport }),
);
activateCanvasGenerationEntry('music');
}, [
activateCanvasGenerationEntry,
canvasSize,
openPlacedCanvasGenerationDialog,
viewport,
]);
const openEditDialog = useCallback(
(sourceLayer: CanvasLayer) => {
setMetadataLayer(null);
setImageContextMenu(null);
setQuickEditPanel(null);
setCropExpandPanel(null);
setUiAssetExtractionState(null);
archiveActiveCanvasGenerationDialog();
setGenerateDialog(createEditDialogDraft(sourceLayer));
setActiveTool('generate');
},
[
archiveActiveCanvasGenerationDialog,
setActiveTool,
setGenerateDialog,
setImageContextMenu,
setMetadataLayer,
],
);
const createQuickEditModeDraft = useCallback(
(sourceLayer: CanvasLayer) => {
const sourceDialog = findSourceGenerationDialog(
canvasGenerationDialogs,
sourceLayer,
);
return createQuickEditPanelDraft(sourceLayer, {
imageModel: sourceDialog?.imageModel,
aspectRatio: sourceDialog?.aspectRatio,
imageSize: sourceDialog?.imageSize,
});
},
[canvasGenerationDialogs],
);
const openQuickEditPanel = useCallback(
(sourceLayer: CanvasLayer) => {
if (isQuickEditUnsupportedAssetKind(sourceLayer)) {
return;
}
setImageContextMenu(null);
setMetadataLayer(null);
setUiAssetExtractionState(null);
setCropExpandPanel(null);
setCharacterAnimationPanel(null);
const quickEditDraft = createQuickEditModeDraft(sourceLayer);
archiveActiveCanvasGenerationDialog();
setGenerateDialog(null);
setQuickEditPanel(quickEditDraft);
selectSingleLayer(sourceLayer.id);
const nextSelectionState = createUiAssetExtractionState(sourceLayer.id, {
initialTool: null,
model: DEFAULT_IMAGE_MODEL,
});
quickEditSelectionStateRef.current = nextSelectionState;
setQuickEditSelectionState(nextSelectionState);
quickEditSelectionMarkCounterRef.current = 0;
setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize }));
setActiveTool('generate');
},
[
archiveActiveCanvasGenerationDialog,
canvasSize,
createQuickEditModeDraft,
selectSingleLayer,
setActiveTool,
setGenerateDialog,
setImageContextMenu,
setMetadataLayer,
setQuickEditPanel,
setViewport,
],
);
const updateSourceLayer = useCallback(
(
sourceLayerId: string,
updater: (layer: CanvasLayer) => CanvasLayer,
options: { fit?: boolean; persist?: boolean } = {},
) => {
const sourceLayer = layers.find((layer) => layer.id === sourceLayerId);
if (!sourceLayer) {
return;
}
const updatedLayer = updater(sourceLayer);
captureCanvasHistory({ type: 'replace-image', count: 1 });
setLayers((currentLayers) =>
currentLayers.map((layer) =>
layer.id === sourceLayerId ? updatedLayer : layer,
),
);
if (options.persist !== false) {
persistUpdatedLayerResource?.(updatedLayer);
}
selectSingleLayer(sourceLayerId);
if (options.fit !== false) {
fitLayers([updatedLayer], { captureHistory: false });
}
},
[
captureCanvasHistory,
fitLayers,
layers,
persistUpdatedLayerResource,
selectSingleLayer,
setLayers,
],
);
const openRedrawPanel = useCallback(
(sourceLayer: CanvasLayer) => {
setImageContextMenu(null);
setMetadataLayer(null);
setCropExpandPanel(null);
setCharacterAnimationPanel(null);
setUiAssetExtractionState(null);
setQuickEditSelectionState(null);
if (sourceLayer.mediaType === 'audio') {
const audioDraft = createAudioRedrawGenerationDialogDraft(sourceLayer);
if (!audioDraft) {
return;
}
setQuickEditPanel(null);
selectSingleLayer(sourceLayer.id);
openCanvasGenerationDialog(audioDraft);
setActiveTool('music');
return;
}
if (sourceLayer.mediaType === 'video') {
const videoDraft = createVideoRedrawGenerationDialogDraft(sourceLayer);
if (!videoDraft) {
return;
}
setQuickEditPanel(null);
selectSingleLayer(sourceLayer.id);
openCanvasGenerationDialog(videoDraft);
setActiveTool('video');
return;
}
if (
sourceLayer.mediaType === undefined ||
sourceLayer.mediaType === 'image' ||
sourceLayer.assetKind === 'character-animation'
) {
const sourceDialog = findSourceGenerationDialog(
canvasGenerationDialogs,
sourceLayer,
);
const sourceAnimationLayer = findSourceAnimationLayer(
layers,
sourceLayer,
);
const sameSourceDraft = createSameSourceGenerationDialogDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
mode: 'redraw',
sourceAnimationLayer,
});
if (sameSourceDraft) {
openPlacedCanvasGenerationDialog(sameSourceDraft);
setQuickEditPanel(null);
selectSingleLayer(null);
setActiveTool(getCanvasToolForGenerationMode(sameSourceDraft.mode));
return;
}
if (sourceLayer.assetKind === 'character-animation') {
setGenerationWarning('未找到角色动作关联的原角色图层,无法改造。');
return;
}
const redrawPanel = createRedrawPanelDraft(sourceLayer, {
imageModel: sourceDialog?.imageModel,
aspectRatio: sourceDialog?.aspectRatio,
imageSize: sourceDialog?.imageSize,
});
const redrawSize = resolveEditorImageGenerationPixelSize({
model: redrawPanel.model,
aspectRatio: redrawPanel.aspectRatio,
imageSize: redrawPanel.imageSize,
});
openPlacedCanvasGenerationDialog({
...createQuickEditGenerationDialogDraft({
sourceLayer,
prompt: redrawPanel.prompt,
model: redrawPanel.model,
aspectRatio: redrawPanel.aspectRatio,
imageSize: redrawPanel.imageSize,
frame: {
width: redrawSize.width,
height: redrawSize.height,
},
}),
composerOpen: true,
});
setQuickEditPanel(null);
selectSingleLayer(null);
setActiveTool('generate');
return;
}
},
[
openCanvasGenerationDialog,
openPlacedCanvasGenerationDialog,
selectSingleLayer,
setActiveTool,
setImageContextMenu,
setMetadataLayer,
canvasGenerationDialogs,
canvasSize,
layers,
viewport,
],
);
const openCropExpandPanel = useCallback(
(sourceLayer: CanvasLayer) => {
setImageContextMenu(null);
setMetadataLayer(null);
setUiAssetExtractionState(null);
archiveActiveCanvasGenerationDialog();
setGenerateDialog(null);
setQuickEditPanel(null);
setCharacterAnimationPanel(null);
setCropExpandPanel({
sourceLayerId: sourceLayer.id,
frame: {
x: sourceLayer.x,
y: sourceLayer.y,
width: sourceLayer.width,
height: sourceLayer.height,
},
ratio: 'free',
status: 'idle',
});
selectSingleLayer(sourceLayer.id);
setActiveTool('select');
},
[
archiveActiveCanvasGenerationDialog,
selectSingleLayer,
setActiveTool,
setGenerateDialog,
setImageContextMenu,
setMetadataLayer,
],
);
const startCropExpandFrameResize = useCallback(
(
event: ReactPointerEvent<HTMLButtonElement>,
handle: CropExpandResizeHandle,
) => {
const currentPanel = cropExpandPanelRef.current;
if (!currentPanel) {
return;
}
event.preventDefault();
event.stopPropagation();
cropExpandResizeDragRef.current = {
pointerId: event.pointerId,
handle,
sourceLayerId: currentPanel.sourceLayerId,
startClientX: event.clientX,
startClientY: event.clientY,
startScale:
Number.isFinite(viewport.scale) && viewport.scale > 0
? viewport.scale
: 1,
startFrame: currentPanel.frame,
ratio: currentPanel.ratio,
};
selectSingleLayer(currentPanel.sourceLayerId);
},
[selectSingleLayer, viewport.scale],
);
useEffect(() => {
const handlePointerMove = (event: PointerEvent) => {
const dragState = cropExpandResizeDragRef.current;
if (!dragState || dragState.pointerId !== event.pointerId) {
return;
}
event.preventDefault();
const deltaX =
(event.clientX - dragState.startClientX) / dragState.startScale;
const deltaY =
(event.clientY - dragState.startClientY) / dragState.startScale;
const nextFrame = resizeCropExpandFrame({
frame: dragState.startFrame,
handle: dragState.handle,
deltaX,
deltaY,
ratio: dragState.ratio,
});
setCropExpandPanel((currentPanel) =>
currentPanel?.sourceLayerId === dragState.sourceLayerId
? {
...currentPanel,
frame: nextFrame,
status:
currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
errorMessage:
currentPanel.status === 'failed'
? undefined
: currentPanel.errorMessage,
}
: currentPanel,
);
};
const handlePointerUp = (event: PointerEvent) => {
const dragState = cropExpandResizeDragRef.current;
if (!dragState || dragState.pointerId !== event.pointerId) {
return;
}
cropExpandResizeDragRef.current = null;
};
window.addEventListener('pointermove', handlePointerMove);
window.addEventListener('pointerup', handlePointerUp);
window.addEventListener('pointercancel', handlePointerUp);
return () => {
window.removeEventListener('pointermove', handlePointerMove);
window.removeEventListener('pointerup', handlePointerUp);
window.removeEventListener('pointercancel', handlePointerUp);
};
}, [setCropExpandPanel]);
const submitCropExpand = useCallback(async () => {
if (!cropExpandPanel || !cropExpandSourceLayer) {
return;
}
try {
const insets = buildCropExpandInsetsFromFrame({
layer: cropExpandSourceLayer,
frame: cropExpandPanel.frame,
});
setCropExpandPanel({
...cropExpandPanel,
status: 'processing',
errorMessage: undefined,
});
const sourceImageSrc = await resolveEditorImageReferenceDataUrl(
cropExpandSourceLayer.objectKey?.trim() || cropExpandSourceLayer.src,
);
const result = await renderCropExpandImage({
source: sourceImageSrc,
insets,
});
layerCounterRef.current += 1;
const cropExpandIndex = layerCounterRef.current;
const cropExpandTitle = `${cropExpandSourceLayer.title} 裁扩`;
const cropExpandProjectId = projectId?.trim();
let cropExpandResourceId = `local-resource-crop-expand-${cropExpandIndex}`;
let cropExpandImageSrc = result.imageSrc;
let cropExpandObjectKey: string | null = null;
let cropExpandAssetObjectId: string | null = null;
let cropExpandSourceResourceId: string | null =
cropExpandSourceLayer.resourceId;
let cropExpandAssetKind = cropExpandSourceLayer.assetKind;
if (cropExpandProjectId) {
const cropExpandUploadId = createEditorGenerationMediaUploadId();
const cropExpandFile = dataUrlToImageFile(
result.imageSrc,
`crop-expand-${cropExpandIndex}-${cropExpandUploadId}.png`,
);
const uploadedCropExpand = await uploadEditorMediaAssetFile(
cropExpandFile,
'image',
{
assetKind: 'editor_crop_expand_image',
pathSegments: [
'editor',
'crop-expand',
cropExpandProjectId,
cropExpandUploadId,
],
entityId: cropExpandProjectId,
metadata: {
editor_project_id: cropExpandProjectId,
source_resource_id: cropExpandSourceLayer.resourceId,
},
},
);
const cropExpandResource = await createEditorProjectResource(
cropExpandProjectId,
{
imageSrc: uploadedCropExpand.legacyPublicPath,
objectKey: uploadedCropExpand.objectKey,
assetObjectId: uploadedCropExpand.assetObjectId,
width: result.width,
height: result.height,
sourceType: 'generated',
prompt: cropExpandSourceLayer.prompt,
actualPrompt: cropExpandSourceLayer.actualPrompt,
model: cropExpandSourceLayer.model,
provider: cropExpandSourceLayer.provider,
taskId: cropExpandSourceLayer.taskId,
sourceResourceId: cropExpandSourceLayer.resourceId,
assetKind: cropExpandSourceLayer.assetKind,
generationInputs: cropExpandSourceLayer.generationInputs,
},
);
cropExpandResourceId = cropExpandResource.resourceId;
cropExpandImageSrc = cropExpandResource.imageSrc;
cropExpandObjectKey =
cropExpandResource.objectKey ?? uploadedCropExpand.objectKey;
cropExpandAssetObjectId =
cropExpandResource.assetObjectId ?? uploadedCropExpand.assetObjectId;
cropExpandSourceResourceId =
cropExpandResource.sourceResourceId ??
cropExpandSourceLayer.resourceId;
cropExpandAssetKind =
(cropExpandResource.assetKind as CanvasLayer['assetKind']) ??
cropExpandSourceLayer.assetKind;
}
const nextLayer: CanvasLayer = {
...cropExpandSourceLayer,
id: `layer-crop-expand-${cropExpandIndex}`,
resourceId: cropExpandResourceId,
title: cropExpandTitle,
src: cropExpandImageSrc,
x: cropExpandSourceLayer.x + cropExpandSourceLayer.width + 32,
y: cropExpandSourceLayer.y,
width: result.width,
height: result.height,
originalWidth: result.width,
originalHeight: result.height,
zIndex: cropExpandIndex + 10,
sourceType: 'generated',
sourceResourceId: cropExpandSourceResourceId,
objectKey: cropExpandObjectKey,
assetObjectId: cropExpandAssetObjectId,
sourceAssetId: null,
assetKind: cropExpandAssetKind,
};
captureCanvasHistory({ type: 'expand-image', count: 1 });
appendCanvasLayersWithResources([nextLayer]);
persistGeneratedAsset?.(nextLayer);
selectSingleLayer(nextLayer.id);
fitLayers([cropExpandSourceLayer, nextLayer], { captureHistory: false });
setCropExpandPanel(null);
setActiveSidebarPanel('layers');
} catch (error) {
setCropExpandPanel({
...cropExpandPanel,
status: 'failed',
errorMessage:
error instanceof Error && error.message.trim()
? error.message
: '裁扩图片失败',
});
}
}, [
cropExpandPanel,
cropExpandSourceLayer,
appendCanvasLayersWithResources,
captureCanvasHistory,
fitLayers,
layerCounterRef,
persistGeneratedAsset,
projectId,
selectSingleLayer,
setActiveSidebarPanel,
]);
const removeSelectedLayerBackground = useCallback(
async (sourceLayer: CanvasLayer) => {
setImageContextMenu(null);
setMetadataLayer(null);
setCropExpandPanel(null);
setQuickEditPanel(null);
const assetLabel = `${sourceLayer.title} 去背景`;
const backgroundRemovalDialog =
projectId && applyProjectSnapshot
? createQuickEditGenerationDialogDraft({
sourceLayer,
prompt: '去除背景',
status: 'generating',
frame: {
width: sourceLayer.width,
height: sourceLayer.height,
},
})
: null;
const backgroundRemovalPlacement = backgroundRemovalDialog
? openPlacedCanvasGenerationDialog({
...backgroundRemovalDialog,
composerOpen: false,
})
: undefined;
const backgroundRemovalDialogId = backgroundRemovalPlacement?.dialogId;
const applyBackgroundRemovalProjectSnapshot =
projectId && applyProjectSnapshot
? (project: EditorProjectSnapshot) => {
applyProjectSnapshot(
preserveSourceLayerInProjectSnapshot(project, sourceLayer),
);
}
: undefined;
try {
const sourceImageSrc = await resolveEditorGenerationMediaReference(
sourceLayer,
'image',
projectId,
);
const result = await removeImageBackground({
sourceImageSrc,
projectId,
targetLayerId: sourceLayer.id,
assetKind: sourceLayer.assetKind,
generationInputs: sourceLayer.generationInputs,
assetFolderId,
assetLabel,
sourceResourceId: sourceLayer.resourceId,
...(backgroundRemovalPlacement?.placeholder
? {
canvasCompletion: {
dialogId: backgroundRemovalDialogId,
title: assetLabel,
placeholder: backgroundRemovalPlacement.placeholder,
},
}
: {}),
});
await applyQueuedEditorGenerationProject(
result,
projectId,
(project) =>
applyProjectSnapshot?.(project, {
type: 'remove-background',
count: 1,
}),
refreshTaskListForQueuedGeneration,
onWalletBalanceMayHaveChanged,
setGenerationWarning,
backgroundRemovalDialogId,
applyBackgroundRemovalProjectSnapshot
? (project) =>
preserveSourceLayerInProjectSnapshot(project, sourceLayer)
: undefined,
);
} catch (error) {
if (backgroundRemovalDialogId) {
updateCanvasGenerationDialogById(
backgroundRemovalDialogId,
(dialog) => ({
...dialog,
status: 'failed',
errorMessage:
error instanceof Error && error.message.trim()
? error.message
: '去除背景失败',
}),
);
}
throw error;
}
},
[
applyProjectSnapshot,
assetFolderId,
onWalletBalanceMayHaveChanged,
openPlacedCanvasGenerationDialog,
projectId,
refreshTaskListForQueuedGeneration,
setImageContextMenu,
setMetadataLayer,
updateCanvasGenerationDialogById,
],
);
const splitSelectedIconSpritesheet = useCallback(
async (sourceLayer: CanvasLayer) => {
if (
sourceLayer.assetKind !== 'icon-spritesheet' ||
!projectId ||
!applyProjectSnapshot ||
splittingIconSpritesheetLayerIdsRef.current.has(sourceLayer.id)
) {
return;
}
splittingIconSpritesheetLayerIdsRef.current.add(sourceLayer.id);
closeGenerationTransientState();
setImageContextMenu(null);
setMetadataLayer(null);
setCropExpandPanel(null);
setQuickEditPanel(null);
try {
const result = await splitEditorIconSpritesheet({
projectId,
sourceResourceId: sourceLayer.resourceId,
assetFolderId,
canvasCompletion: {
title: '拆分图集',
placeholder: {
x: sourceLayer.x,
y: sourceLayer.y,
width: sourceLayer.width,
height: sourceLayer.height,
originalWidth: sourceLayer.originalWidth,
originalHeight: sourceLayer.originalHeight,
},
},
});
applyProjectSnapshot(result.project, {
type: 'split-atlas',
count: 1,
});
setActiveTool('select');
setActiveSidebarPanel('layers');
} catch (error) {
window.alert(
error instanceof Error && error.message.trim()
? error.message
: '拆分图集失败',
);
} finally {
splittingIconSpritesheetLayerIdsRef.current.delete(sourceLayer.id);
}
},
[
applyProjectSnapshot,
assetFolderId,
closeGenerationTransientState,
projectId,
setActiveSidebarPanel,
setActiveTool,
setCropExpandPanel,
setImageContextMenu,
setMetadataLayer,
setQuickEditPanel,
],
);
const pickCharacterSpecFromLayer = useCallback(
(layer: CanvasLayer) => {
if (!isCharacterSpecReferenceLayer(layer)) {
setGenerationWarning(INVALID_CHARACTER_SPEC_WARNING);
return;
}
setGenerationWarning(null);
setGenerateDialog((currentDialog) => {
const nextDialog = assignCharacterSpecReference(currentDialog, layer);
if (
nextDialog?.mode === 'character' &&
nextDialog.characterSpecReference
) {
lastCharacterSpecReferenceRef.current =
nextDialog.characterSpecReference;
}
return nextDialog;
});
setIsPickingCharacterSpecFromCanvas(false);
setIsCharacterSpecMenuOpen(false);
setImageContextMenu(null);
},
[setGenerateDialog, setImageContextMenu],
);
const pickGenerationReferenceFromLayer = useCallback(
(layer: CanvasLayer) => {
setGenerateDialog((currentDialog) =>
appendGenerationReference(currentDialog, layer),
);
setIsPickingGenerationReferenceFromCanvas(false);
setIsGenerationReferenceMenuOpen(false);
setImageContextMenu(null);
},
[setGenerateDialog, setImageContextMenu],
);
const pickQuickEditReferenceFromLayer = useCallback(
(layer: CanvasLayer) => {
setGenerateDialog((currentDialog) =>
appendGenerationReference(currentDialog, layer),
);
setIsPickingQuickEditReferenceFromCanvas(false);
setIsGenerationReferenceMenuOpen(false);
setImageContextMenu(null);
},
[setGenerateDialog, setImageContextMenu],
);
const pickCharacterReferenceFromLayer = useCallback(
(layer: CanvasLayer) => {
setGenerateDialog((currentDialog) =>
appendCharacterReference(currentDialog, layer),
);
setIsPickingCharacterReferenceFromCanvas(false);
setImageContextMenu(null);
},
[setGenerateDialog, setImageContextMenu],
);
const pickIconSpecFromLayer = useCallback(
(layer: CanvasLayer) => {
if (!isIconSpecReferenceLayer(layer)) {
setGenerationWarning(INVALID_ICON_SPEC_WARNING);
return;
}
setGenerationWarning(null);
const nextDialog = assignIconSpecReference(generateDialog, layer);
if (nextDialog?.mode === 'icon' && nextDialog.iconSpecReference) {
lastIconSpecReferenceRef.current = nextDialog.iconSpecReference;
}
setGenerateDialog(nextDialog);
if (nextDialog === generateDialog) {
return;
}
setIsPickingIconSpecFromCanvas(false);
setIsIconSpecMenuOpen(false);
setImageContextMenu(null);
},
[generateDialog, setGenerateDialog, setImageContextMenu],
);
const pickUiDesignSpecFromLayer = useCallback(
(layer: CanvasLayer) => {
if (!isIconSpecReferenceLayer(layer)) {
setGenerationWarning(INVALID_ICON_SPEC_WARNING);
return;
}
setGenerationWarning(null);
const nextDialog = assignUiDesignSpecReference(generateDialog, layer);
if (
nextDialog?.mode === 'ui-design' &&
nextDialog.uiDesignSpecReference
) {
lastIconSpecReferenceRef.current = nextDialog.uiDesignSpecReference;
}
setGenerateDialog(nextDialog);
if (nextDialog === generateDialog) {
return;
}
setIsPickingUiDesignSpecFromCanvas(false);
setIsUiDesignSpecMenuOpen(false);
setImageContextMenu(null);
},
[generateDialog, setGenerateDialog, setImageContextMenu],
);
const pickPublicationReferenceFromLayer = useCallback(
(layer: CanvasLayer) => {
setGenerateDialog((currentDialog) =>
appendPublicationReference(currentDialog, layer),
);
setIsPickingPublicationReferenceFromCanvas(false);
setIsPublicationReferenceMenuOpen(false);
setImageContextMenu(null);
},
[setGenerateDialog, setImageContextMenu],
);
const updateIconDescriptionsText = useCallback(
(value: string) => {
setGenerateDialog((currentDialog) =>
updateIconDescriptionsTextInDialog(currentDialog, value),
);
},
[setGenerateDialog],
);
const rememberImageModel = useCallback((model: string) => {
setRememberedImageOptions((currentOptions) =>
normalizeRememberedImageGenerationOptions({
...currentOptions,
model,
}),
);
}, []);
const generationSubmissionWorkflow =
useImageCanvasGenerationSubmissionWorkflow({
layers,
canvasSize,
viewport,
layerCounterRef,
quickEditPanel,
quickEditSourceLayer,
quickEditSelectionState,
canvasGenerationDialogs,
setQuickEditPanel,
characterAnimationPanel: effectiveCharacterAnimationPanel,
characterAnimationDialog,
characterAnimationSourceLayer,
setCharacterAnimationPanel,
setGenerateDialog,
openCanvasGenerationDialog,
updateCanvasGenerationDialogById,
hasCanvasGenerationDialogById,
getGeneratingDialogPlaceholder,
appendCanvasLayersWithResources,
captureCanvasHistory,
updateSourceLayer,
selectSingleLayer,
fitLayers,
setActiveTool,
setActiveSidebarPanel,
rememberImageModel,
projectId,
assetFolderId,
upsertGeneratedAsset,
applyProjectSnapshot,
onQueuedGenerationTask: refreshTaskListForQueuedGeneration,
onWalletBalanceMayHaveChanged,
onGenerationWarning: setGenerationWarning,
});
const setEffectiveCharacterAnimationPanel: Dispatch<
SetStateAction<CharacterAnimationPanelState | null>
> = useCallback(
(updater) => {
setGenerateDialog((currentDialog) => {
if (
!isCanvasGenerationDialog(currentDialog) ||
currentDialog.mode !== 'character-animation'
) {
return currentDialog;
}
const currentPanel =
createCharacterAnimationPanelFromDialog(currentDialog);
const nextPanel =
typeof updater === 'function' ? updater(currentPanel) : updater;
return applyCharacterAnimationPanelToDialog(currentDialog, nextPanel);
});
setCharacterAnimationPanel((currentPanel) => {
if (characterAnimationDialog) {
return currentPanel;
}
return typeof updater === 'function' ? updater(currentPanel) : updater;
});
},
[characterAnimationDialog, setGenerateDialog],
);
const {
extractUiDesignAssets: submitUiDesignAssetExtraction,
submitCharacterAnimation,
submitIconSpritesheetGeneration,
submitImageGeneration,
submitQuickEdit,
} = generationSubmissionWorkflow;
const extractUiDesignAssets = useCallback(
(sourceLayer: CanvasLayer) => {
if (sourceLayer.assetKind !== 'ui-design') {
return;
}
closeGenerationTransientState();
setMetadataLayer(null);
setQuickEditPanel(null);
setCropExpandPanel(null);
setCharacterAnimationPanel(null);
selectSingleLayer(sourceLayer.id);
setActiveTool('select');
setUiAssetExtractionState(
createUiAssetExtractionState(sourceLayer.id, {
initialTool: 'rect',
model: DEFAULT_IMAGE_MODEL,
}),
);
setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize }));
},
[
canvasSize,
closeGenerationTransientState,
selectSingleLayer,
setActiveTool,
setCharacterAnimationPanel,
setCropExpandPanel,
setMetadataLayer,
setQuickEditPanel,
setViewport,
],
);
const changeUiAssetExtractionTool = useCallback(
(tool: UiAssetExtractionTool | null) => {
setUiAssetExtractionState((currentState) =>
currentState
? {
...currentState,
tool: currentState.tool === tool ? null : tool,
draftMark: null,
status:
currentState.status === 'failed' ? 'idle' : currentState.status,
errorMessage:
currentState.status === 'failed'
? undefined
: currentState.errorMessage,
}
: currentState,
);
},
[],
);
const changeUiAssetExtractionModel = useCallback(
(model: string) => {
const normalizedModel = normalizeEditorImageModel(model);
rememberImageModel(normalizedModel);
setUiAssetExtractionState((currentState) =>
currentState
? {
...currentState,
model: normalizedModel,
status:
currentState.status === 'failed' ? 'idle' : currentState.status,
errorMessage:
currentState.status === 'failed'
? undefined
: currentState.errorMessage,
}
: currentState,
);
},
[rememberImageModel],
);
const appendUiAssetExtractionReferences = useCallback(
(references: CharacterReferenceImage[]) => {
if (!references.length) {
return;
}
setUiAssetExtractionState((currentState) =>
currentState
? {
...currentState,
references: [...currentState.references, ...references],
status:
currentState.status === 'failed' ? 'idle' : currentState.status,
errorMessage:
currentState.status === 'failed'
? undefined
: currentState.errorMessage,
}
: currentState,
);
},
[],
);
const removeUiAssetExtractionReference = useCallback(
(referenceId: string) => {
setUiAssetExtractionState((currentState) =>
currentState
? {
...currentState,
references: currentState.references.filter(
(reference) => reference.id !== referenceId,
),
status:
currentState.status === 'failed' ? 'idle' : currentState.status,
errorMessage:
currentState.status === 'failed'
? undefined
: currentState.errorMessage,
}
: currentState,
);
},
[],
);
const changeQuickEditSelectionTool = useCallback(
(tool: UiAssetExtractionTool | null) => {
updateQuickEditSelectionState((currentState) =>
currentState
? {
...currentState,
tool: currentState.tool === tool ? null : tool,
draftMark: null,
status: 'idle',
errorMessage: undefined,
}
: currentState,
);
},
[updateQuickEditSelectionState],
);
const startUiAssetExtractionPointer = useCallback(
(point: { x: number; y: number }) => {
setUiAssetExtractionState((currentState) => {
if (
!currentState ||
!currentState.tool ||
currentState.status === 'extracting'
) {
return currentState;
}
uiAssetExtractionMarkCounterRef.current += 1;
return {
...currentState,
draftMark: createUiAssetExtractionDraftMark({
tool: currentState.tool,
id: `ui-extraction-mark-${uiAssetExtractionMarkCounterRef.current}`,
point,
}),
status: 'idle',
errorMessage: undefined,
};
});
},
[],
);
const startQuickEditSelectionPointer = useCallback(
(point: { x: number; y: number }) => {
updateQuickEditSelectionState((currentState) => {
if (!currentState || !currentState.tool) {
return currentState;
}
quickEditSelectionMarkCounterRef.current += 1;
return {
...currentState,
draftMark: createUiAssetExtractionDraftMark({
tool: currentState.tool,
id: `quick-edit-selection-mark-${quickEditSelectionMarkCounterRef.current}`,
point,
}),
status: 'idle',
errorMessage: undefined,
};
});
},
[updateQuickEditSelectionState],
);
const moveUiAssetExtractionPointer = useCallback(
(point: { x: number; y: number }) => {
setUiAssetExtractionState((currentState) =>
currentState?.draftMark
? {
...currentState,
draftMark: updateUiAssetExtractionDraftMark(
currentState.draftMark,
point,
),
}
: currentState,
);
},
[],
);
const moveQuickEditSelectionPointer = useCallback(
(point: { x: number; y: number }) => {
updateQuickEditSelectionState((currentState) =>
currentState?.draftMark
? {
...currentState,
draftMark: updateUiAssetExtractionDraftMark(
currentState.draftMark,
point,
),
}
: currentState,
);
},
[updateQuickEditSelectionState],
);
const endUiAssetExtractionPointer = useCallback(() => {
setUiAssetExtractionState((currentState) => {
if (!currentState?.draftMark) {
return currentState;
}
const normalizedMark = normalizeUiAssetExtractionMark(
currentState.draftMark,
);
return {
...currentState,
draftMark: null,
marks: normalizedMark
? [...currentState.marks, normalizedMark]
: currentState.marks,
};
});
}, []);
const cancelUiAssetExtractionPointer = useCallback(() => {
setUiAssetExtractionState((currentState) =>
currentState?.draftMark
? {
...currentState,
draftMark: null,
}
: currentState,
);
}, []);
const endQuickEditSelectionPointer = useCallback(() => {
const currentSelectionState = quickEditSelectionStateRef.current;
if (!currentSelectionState?.draftMark) {
return;
}
const normalizedMark = normalizeUiAssetExtractionMark(
currentSelectionState.draftMark,
);
if (!normalizedMark) {
updateQuickEditSelectionState(() => ({
...currentSelectionState,
draftMark: null,
}));
return;
}
const nextMarkNumber = currentSelectionState.marks.length + 1;
updateQuickEditSelectionState(() => ({
...currentSelectionState,
draftMark: null,
marks: [...currentSelectionState.marks, normalizedMark],
}));
setQuickEditPanel((currentPanel) =>
currentPanel?.sourceLayerId === currentSelectionState.sourceLayerId
? {
...currentPanel,
status:
currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
errorMessage:
currentPanel.status === 'failed'
? undefined
: currentPanel.errorMessage,
prompt: appendQuickEditSelectionPrompt(
currentPanel.prompt,
nextMarkNumber,
),
}
: currentPanel,
);
setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'quick-edit' &&
currentDialog.sourceLayerId === currentSelectionState.sourceLayerId
? {
...currentDialog,
status:
currentDialog.status === 'failed' ? 'idle' : currentDialog.status,
errorMessage:
currentDialog.status === 'failed'
? undefined
: currentDialog.errorMessage,
prompt: appendQuickEditSelectionPrompt(
currentDialog.prompt,
nextMarkNumber,
),
composerOpen: true,
}
: currentDialog,
);
}, [setGenerateDialog, setQuickEditPanel, updateQuickEditSelectionState]);
const cancelQuickEditSelectionPointer = useCallback(() => {
updateQuickEditSelectionState((currentState) =>
currentState?.draftMark
? {
...currentState,
draftMark: null,
}
: currentState,
);
}, [updateQuickEditSelectionState]);
useEffect(() => {
if (!quickEditSelectionState) {
return;
}
const hasActiveQuickEditDialog =
generateDialog?.mode === 'quick-edit' &&
generateDialog.sourceLayerId === quickEditSelectionState.sourceLayerId;
if (!hasActiveQuickEditDialog && !quickEditPanel) {
quickEditSelectionStateRef.current = null;
setQuickEditSelectionState(null);
}
}, [generateDialog, quickEditPanel, quickEditSelectionState]);
const cancelUiAssetExtraction = useCallback(() => {
setUiAssetExtractionState(null);
}, []);
const submitUiAssetExtraction = useCallback(async () => {
const currentState = uiAssetExtractionState;
const sourceLayer = uiAssetExtractionSourceLayer;
if (!currentState || !sourceLayer || !currentState.marks.length) {
return;
}
setUiAssetExtractionState({
...currentState,
status: 'extracting',
errorMessage: undefined,
});
try {
await submitUiDesignAssetExtraction(sourceLayer, {
marks: currentState.marks,
model: currentState.model,
references: currentState.references,
suppressAlert: true,
});
setUiAssetExtractionState(null);
} catch (error) {
setUiAssetExtractionState((latestState) =>
latestState?.sourceLayerId === currentState.sourceLayerId
? {
...latestState,
status: 'failed',
errorMessage:
error instanceof Error && error.message.trim()
? error.message
: '提取素材失败',
}
: latestState,
);
}
}, [
submitUiDesignAssetExtraction,
uiAssetExtractionSourceLayer,
uiAssetExtractionState,
]);
const updateSpecFormValue = useCallback(
(key: keyof SpecFormValues, value: string) => {
setGenerateDialog((currentDialog) =>
updateSpecFormDialogValue(currentDialog, key, value),
);
},
[setGenerateDialog],
);
const updateCharacterAnimationDuration = useCallback(
(frameCountValue: string) => {
const option = CHARACTER_ANIMATION_DURATION_OPTIONS.find(
(item) => String(item.frameCount) === frameCountValue,
);
if (option) {
setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'character-animation'
? {
...currentDialog,
characterAnimationFrameCount: option.frameCount,
characterAnimationDurationSeconds: option.durationSeconds,
status:
currentDialog.status === 'failed'
? 'idle'
: currentDialog.status,
errorMessage:
currentDialog.status === 'failed'
? undefined
: currentDialog.errorMessage,
}
: currentDialog,
);
}
setCharacterAnimationPanel((currentPanel) =>
updateCharacterAnimationDurationPanel(currentPanel, frameCountValue),
);
},
[setGenerateDialog],
);
const hideGeneratedLayerPanelAfterBlur = useCallback(() => {
setGenerateDialog((currentDialog) =>
hideGeneratedLayerComposerAfterBlur(currentDialog),
);
}, [setGenerateDialog]);
const closeGenerateComposer = useCallback(() => {
setGenerateDialog(closeGenerateComposerDialog);
setActiveTool('select');
}, [setActiveTool, setGenerateDialog]);
const clearDeletedLayerGenerationState = useCallback(
(targetLayerId: string) => {
setQuickEditPanel((currentPanel) =>
currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel,
);
setQuickEditPanel((currentPanel) =>
currentPanel
? {
...currentPanel,
quickEditReferences: (
currentPanel.quickEditReferences ?? []
).filter(
(reference) => reference.id !== `canvas-${targetLayerId}`,
),
}
: currentPanel,
);
setCropExpandPanel((currentPanel) =>
currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel,
);
setUiAssetExtractionState((currentState) =>
currentState?.sourceLayerId === targetLayerId ? null : currentState,
);
setQuickEditSelectionState((currentState) =>
currentState?.sourceLayerId === targetLayerId ? null : currentState,
);
setCharacterAnimationPanel((currentPanel) =>
currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel,
);
setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'edit' &&
currentDialog.sourceLayerId === targetLayerId
? null
: currentDialog,
);
removeCanvasGenerationDialogsByLayerId(targetLayerId);
},
[removeCanvasGenerationDialogsByLayerId, setGenerateDialog],
);
return useMemo(
() => ({
quickEditPanel,
setQuickEditPanel,
quickEditSourceLayer,
cropExpandPanel,
setCropExpandPanel,
cropExpandSourceLayer,
uiAssetExtractionState,
uiAssetExtractionSourceLayer,
quickEditSelectionState,
quickEditSelectionSourceLayer,
changeUiAssetExtractionTool,
changeUiAssetExtractionModel,
appendUiAssetExtractionReferences,
removeUiAssetExtractionReference,
changeQuickEditSelectionTool,
startUiAssetExtractionPointer,
startQuickEditSelectionPointer,
moveUiAssetExtractionPointer,
moveQuickEditSelectionPointer,
endUiAssetExtractionPointer,
endQuickEditSelectionPointer,
cancelUiAssetExtractionPointer,
cancelQuickEditSelectionPointer,
cancelUiAssetExtraction,
submitUiAssetExtraction,
characterAnimationPanel: effectiveCharacterAnimationPanel,
setCharacterAnimationPanel: setEffectiveCharacterAnimationPanel,
characterAnimationSourceLayer,
characterAnimationPrice,
iconDescriptionValues,
isSpecMenuOpen,
setIsSpecMenuOpen,
isGenerationReferenceMenuOpen,
setIsGenerationReferenceMenuOpen,
isCharacterSpecMenuOpen,
setIsCharacterSpecMenuOpen,
isCharacterReferenceMenuOpen,
setIsCharacterReferenceMenuOpen,
isPickingGenerationReferenceFromCanvas,
setIsPickingGenerationReferenceFromCanvas,
isPickingQuickEditReferenceFromCanvas,
setIsPickingQuickEditReferenceFromCanvas,
isPickingCharacterSpecFromCanvas,
setIsPickingCharacterSpecFromCanvas,
isPickingCharacterReferenceFromCanvas,
setIsPickingCharacterReferenceFromCanvas,
isIconSpecMenuOpen,
setIsIconSpecMenuOpen,
isPickingIconSpecFromCanvas,
setIsPickingIconSpecFromCanvas,
isUiDesignSpecMenuOpen,
setIsUiDesignSpecMenuOpen,
isPickingUiDesignSpecFromCanvas,
setIsPickingUiDesignSpecFromCanvas,
isMusicMenuOpen,
setIsMusicMenuOpen,
isPublicationMenuOpen,
setIsPublicationMenuOpen,
isPublicationReferenceMenuOpen,
setIsPublicationReferenceMenuOpen,
isPickingPublicationReferenceFromCanvas,
setIsPickingPublicationReferenceFromCanvas,
generationWarning,
showGenerationWarning: setGenerationWarning,
clearGenerationWarning: () => setGenerationWarning(null),
openGenerateDialog,
openSpecDialog,
openCharacterAnimationPanel,
openCharacterGenerationDialog,
openIconGenerationDialog,
openPublicationGenerationDialog,
openVideoGenerationDialog,
openUiDesignGenerationDialog,
openSoundEffectGenerationDialog,
openBackgroundMusicGenerationDialog,
openEditDialog,
openQuickEditPanel,
openRedrawPanel,
openCropExpandPanel,
startCropExpandFrameResize,
removeSelectedLayerBackground,
splitSelectedIconSpritesheet,
taskListRefreshKey,
refreshTaskList,
isTaskSidebarOpen,
toggleTaskSidebar: () => setIsTaskSidebarOpen((open) => !open),
extractUiDesignAssets,
pickCharacterSpecFromLayer,
pickGenerationReferenceFromLayer,
pickQuickEditReferenceFromLayer,
pickCharacterReferenceFromLayer,
pickIconSpecFromLayer,
pickUiDesignSpecFromLayer,
pickPublicationReferenceFromLayer,
submitIconSpritesheetGeneration,
submitQuickEdit,
submitCropExpand,
submitImageGeneration,
updateSpecFormValue,
updateIconDescriptionsText,
updateCharacterAnimationDuration,
rememberImageModel,
submitCharacterAnimation,
hideGeneratedLayerPanelAfterBlur,
closeGenerateComposer,
clearDeletedLayerGenerationState,
}),
[
effectiveCharacterAnimationPanel,
characterAnimationPrice,
characterAnimationSourceLayer,
clearDeletedLayerGenerationState,
closeGenerateComposer,
cropExpandPanel,
cropExpandSourceLayer,
uiAssetExtractionState,
uiAssetExtractionSourceLayer,
quickEditSelectionState,
quickEditSelectionSourceLayer,
changeUiAssetExtractionTool,
changeUiAssetExtractionModel,
appendUiAssetExtractionReferences,
removeUiAssetExtractionReference,
changeQuickEditSelectionTool,
startUiAssetExtractionPointer,
startQuickEditSelectionPointer,
moveUiAssetExtractionPointer,
moveQuickEditSelectionPointer,
endUiAssetExtractionPointer,
endQuickEditSelectionPointer,
cancelUiAssetExtractionPointer,
cancelQuickEditSelectionPointer,
cancelUiAssetExtraction,
submitUiAssetExtraction,
setEffectiveCharacterAnimationPanel,
hideGeneratedLayerPanelAfterBlur,
iconDescriptionValues,
isCharacterReferenceMenuOpen,
isCharacterSpecMenuOpen,
isIconSpecMenuOpen,
isGenerationReferenceMenuOpen,
isPickingCharacterReferenceFromCanvas,
isPickingCharacterSpecFromCanvas,
isPickingGenerationReferenceFromCanvas,
isPickingQuickEditReferenceFromCanvas,
isPickingIconSpecFromCanvas,
isPickingUiDesignSpecFromCanvas,
isUiDesignSpecMenuOpen,
isMusicMenuOpen,
isTaskSidebarOpen,
taskListRefreshKey,
refreshTaskList,
isPublicationMenuOpen,
isPublicationReferenceMenuOpen,
isSpecMenuOpen,
isPickingPublicationReferenceFromCanvas,
generationWarning,
openBackgroundMusicGenerationDialog,
openCharacterAnimationPanel,
openCharacterGenerationDialog,
openCropExpandPanel,
openEditDialog,
extractUiDesignAssets,
openGenerateDialog,
openIconGenerationDialog,
openPublicationGenerationDialog,
openRedrawPanel,
openUiDesignGenerationDialog,
openQuickEditPanel,
openSpecDialog,
startCropExpandFrameResize,
openSoundEffectGenerationDialog,
openVideoGenerationDialog,
pickCharacterReferenceFromLayer,
pickCharacterSpecFromLayer,
pickGenerationReferenceFromLayer,
pickQuickEditReferenceFromLayer,
pickIconSpecFromLayer,
pickPublicationReferenceFromLayer,
pickUiDesignSpecFromLayer,
quickEditPanel,
quickEditSourceLayer,
removeSelectedLayerBackground,
splitSelectedIconSpritesheet,
submitCharacterAnimation,
submitCropExpand,
submitIconSpritesheetGeneration,
submitImageGeneration,
submitQuickEdit,
updateCharacterAnimationDuration,
updateIconDescriptionsText,
updateSpecFormValue,
rememberImageModel,
],
);
}