1ed8064d2f
移除前端从提示词解析素材数量的逻辑,完整提示词作为单个请求元素提交。 删除后端按描述数量截断切片的路径,自动拆分与手动拆分复用全连通域算法。 统一切片命名和尺寸、像素、数量限制,保留手动拆分按钮与接口。 更新既有测试、OpenAPI、编辑器文档和共享决策记录。
2697 lines
83 KiB
TypeScript
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,
|
|
],
|
|
);
|
|
}
|