Files
Genarrative/src/components/image-editor/useImageCanvasGenerationWorkflow.ts
T
lhk229 1ed8064d2f 统一图标图集全连通域拆分
移除前端从提示词解析素材数量的逻辑,完整提示词作为单个请求元素提交。

删除后端按描述数量截断切片的路径,自动拆分与手动拆分复用全连通域算法。

统一切片命名和尺寸、像素、数量限制,保留手动拆分按钮与接口。

更新既有测试、OpenAPI、编辑器文档和共享决策记录。
2026-07-29 04:52:29 +00:00

2697 lines
83 KiB
TypeScript

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_IMAGE_MODEL,
EDITOR_IMAGE_DIMENSION_OPTIONS,
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 [splittingIconSpritesheetLayerIds, setSplittingIconSpritesheetLayerIds] =
useState<Set<string>>(() => new Set());
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 [generationWarningVersion, setGenerationWarningVersion] = useState(0);
const showGenerationWarning = useCallback((warning: string) => {
setGenerationWarning(warning);
setGenerationWarningVersion((version) => version + 1);
}, []);
useEffect(() => {
if (!generationWarning) {
return;
}
const timer = window.setTimeout(() => {
setGenerationWarning(null);
}, 3_000);
return () => {
window.clearTimeout(timer);
};
}, [generationWarning, generationWarningVersion]);
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 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') {
showGenerationWarning('未找到角色动作关联的原角色图层,无法改造。');
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,
showGenerationWarning,
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,
showGenerationWarning,
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,
showGenerationWarning,
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);
setSplittingIconSpritesheetLayerIds((currentLayerIds) => {
const nextLayerIds = new Set(currentLayerIds);
nextLayerIds.add(sourceLayer.id);
return nextLayerIds;
});
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);
setSplittingIconSpritesheetLayerIds((currentLayerIds) => {
if (!currentLayerIds.has(sourceLayer.id)) {
return currentLayerIds;
}
const nextLayerIds = new Set(currentLayerIds);
nextLayerIds.delete(sourceLayer.id);
return nextLayerIds;
});
}
},
[
applyProjectSnapshot,
assetFolderId,
closeGenerationTransientState,
projectId,
setActiveSidebarPanel,
setActiveTool,
setCropExpandPanel,
setImageContextMenu,
setMetadataLayer,
setQuickEditPanel,
],
);
const pickCharacterSpecFromLayer = useCallback(
(layer: CanvasLayer) => {
if (!isCharacterSpecReferenceLayer(layer)) {
showGenerationWarning(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, showGenerationWarning],
);
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)) {
showGenerationWarning(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,
showGenerationWarning,
],
);
const pickUiDesignSpecFromLayer = useCallback(
(layer: CanvasLayer) => {
if (!isIconSpecReferenceLayer(layer)) {
showGenerationWarning(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,
showGenerationWarning,
],
);
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: showGenerationWarning,
});
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,
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,
generationWarningVersion,
showGenerationWarning,
clearGenerationWarning: () => setGenerationWarning(null),
openGenerateDialog,
openSpecDialog,
openCharacterAnimationPanel,
openCharacterGenerationDialog,
openIconGenerationDialog,
openPublicationGenerationDialog,
openVideoGenerationDialog,
openUiDesignGenerationDialog,
openSoundEffectGenerationDialog,
openBackgroundMusicGenerationDialog,
openEditDialog,
openQuickEditPanel,
openRedrawPanel,
openCropExpandPanel,
startCropExpandFrameResize,
removeSelectedLayerBackground,
splitSelectedIconSpritesheet,
splittingIconSpritesheetLayerIds,
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,
isCharacterReferenceMenuOpen,
isCharacterSpecMenuOpen,
isIconSpecMenuOpen,
isGenerationReferenceMenuOpen,
isPickingCharacterReferenceFromCanvas,
isPickingCharacterSpecFromCanvas,
isPickingGenerationReferenceFromCanvas,
isPickingQuickEditReferenceFromCanvas,
isPickingIconSpecFromCanvas,
isPickingUiDesignSpecFromCanvas,
isUiDesignSpecMenuOpen,
isMusicMenuOpen,
isTaskSidebarOpen,
taskListRefreshKey,
refreshTaskList,
isPublicationMenuOpen,
isPublicationReferenceMenuOpen,
isSpecMenuOpen,
isPickingPublicationReferenceFromCanvas,
generationWarning,
generationWarningVersion,
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,
splittingIconSpritesheetLayerIds,
submitCharacterAnimation,
submitCropExpand,
submitIconSpritesheetGeneration,
submitImageGeneration,
submitQuickEdit,
updateCharacterAnimationDuration,
updateIconDescriptionsText,
updateSpecFormValue,
rememberImageModel,
showGenerationWarning,
],
);
}