8eecd038f5
为UI素材提取面板接入图片模型选择器,默认使用nanobanana模型。 提取提交链路传递选中模型并按模型计算泥点价格。 后端UI素材提取请求支持模型字段,队列、执行、持久化和响应统一使用选中模型。 同步外部OpenAPI模型字段并补充前端工作流、客户端请求和后端默认模型测试。
1802 lines
56 KiB
TypeScript
1802 lines
56 KiB
TypeScript
import {
|
|
type Dispatch,
|
|
type MutableRefObject,
|
|
type SetStateAction,
|
|
useCallback,
|
|
useMemo,
|
|
} from 'react';
|
|
|
|
import {
|
|
resolveEditorImageReferenceDataUrl,
|
|
resolveEditorImageReferenceDataUrlForGeneration,
|
|
} from '../../services/image-editor/editorImageReference';
|
|
import type {
|
|
EditorCanvasGenerationCompletionInput,
|
|
EditorAssetSnapshot,
|
|
EditorProjectSnapshot,
|
|
} from '../../services/image-editor/editorProjectClient';
|
|
import {
|
|
editEditorImage,
|
|
extractEditorUiDesignAssets,
|
|
generateEditorBackgroundMusic,
|
|
generateEditorCharacterAnimation,
|
|
generateEditorIconSpritesheet,
|
|
generateEditorImage,
|
|
generateEditorSoundEffect,
|
|
generateEditorVideo,
|
|
loadEditorProject,
|
|
} from '../../services/image-editor/editorProjectClient';
|
|
import { getExternalGenerationJobStatus } from '../../services/external-generation';
|
|
import type { ExternalGenerationJobStatusRecord } from '../../../packages/shared/src/contracts/externalGeneration';
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasGenerationInputs,
|
|
CanvasLayer,
|
|
CanvasTool,
|
|
CanvasViewport,
|
|
CharacterAnimationPanelState,
|
|
GenerateDialogState,
|
|
QuickEditPanelState,
|
|
SidebarPanel,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { createQuickEditGenerationDialogDraft } from './ImageCanvasGenerationDialogModel';
|
|
import {
|
|
createAudioResultLayer,
|
|
createCharacterAnimationResultLayer,
|
|
createGeneratedResultLayer,
|
|
createIconSpritesheetResultLayers,
|
|
createQuickEditResultLayer,
|
|
createVideoResultLayer,
|
|
} from './ImageCanvasGenerationLayerModel';
|
|
import { chooseGenerationPlacement } from './ImageCanvasGenerationPlacementModel';
|
|
import {
|
|
buildCharacterAnimationGenerationInputs,
|
|
buildEditGenerationInputs,
|
|
buildQuickEditGenerationInputs,
|
|
buildUiDesignAssetExtractionGenerationInputs,
|
|
buildVideoGenerationInputs,
|
|
CHARACTER_ANIMATION_MODEL,
|
|
DEFAULT_IMAGE_MODEL,
|
|
DEFAULT_VIDEO_MODEL,
|
|
ICON_FRAME_DISPLAY_SIZE,
|
|
ICON_FRAME_ORIGINAL_SIZE,
|
|
inferEditorImageAspectRatio,
|
|
inferEditorImageSizeLabel,
|
|
isCanvasGenerationDialog,
|
|
normalizeEditorImageModel,
|
|
normalizeQuickEditPromptSourceReference,
|
|
resolveCharacterAnimationSourceImageSrc,
|
|
resolveEditorImageGenerationPixelSize,
|
|
resolveImageGenerationErrorMessage,
|
|
} from './ImageCanvasGenerationModel';
|
|
import {
|
|
buildCharacterAnimationSubmissionPlan,
|
|
buildIconSpritesheetGenerationSubmissionPlan,
|
|
buildImageGenerationSubmissionPlan,
|
|
} from './ImageCanvasGenerationSubmissionModel';
|
|
import type {
|
|
UiAssetExtractionMark,
|
|
UiAssetExtractionState,
|
|
} from './ImageCanvasUiAssetExtractionModel';
|
|
import { resolveUiAssetExtractionGenerationPlan } from './ImageCanvasUiAssetExtractionModel';
|
|
import { renderUiDesignAssetExtractionMarkedImage } from './ImageCanvasUiAssetExtractionRasterModel';
|
|
|
|
type CanvasSize = { width: number; height: number };
|
|
|
|
type CanvasGenerationDialogUpdater = (
|
|
dialog: CanvasGenerationDialogState,
|
|
) => CanvasGenerationDialogState | null;
|
|
|
|
type UiDesignAssetExtractionOptions = {
|
|
marks?: UiAssetExtractionMark[];
|
|
model?: string;
|
|
suppressAlert?: boolean;
|
|
};
|
|
|
|
const EDITOR_GENERATION_QUEUE_POLL_INTERVAL_MS = 1600;
|
|
const EDITOR_GENERATION_QUEUE_TIMEOUT_MS = 20 * 60 * 1000;
|
|
|
|
type GenerationSubmissionWorkflowOptions = {
|
|
layers: CanvasLayer[];
|
|
canvasSize: CanvasSize;
|
|
viewport: CanvasViewport;
|
|
layerCounterRef: MutableRefObject<number>;
|
|
quickEditPanel: QuickEditPanelState | null;
|
|
quickEditSourceLayer: CanvasLayer | null;
|
|
quickEditSelectionState: UiAssetExtractionState | null;
|
|
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
|
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
|
|
characterAnimationPanel: CharacterAnimationPanelState | null;
|
|
characterAnimationDialog: CanvasGenerationDialogState | null;
|
|
characterAnimationSourceLayer: CanvasLayer | null;
|
|
setCharacterAnimationPanel: Dispatch<
|
|
SetStateAction<CharacterAnimationPanelState | null>
|
|
>;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
openCanvasGenerationDialog: (
|
|
dialog: Omit<CanvasGenerationDialogState, 'id'>,
|
|
) => string;
|
|
updateCanvasGenerationDialogById: (
|
|
dialogId: string,
|
|
updater: CanvasGenerationDialogUpdater,
|
|
) => void;
|
|
hasCanvasGenerationDialogById: (dialogId: string) => boolean;
|
|
getGeneratingDialogPlaceholder: (
|
|
dialog: GenerateDialogState,
|
|
) => GenerateDialogState['placeholder'];
|
|
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
|
|
selectSingleLayer: (layerId: string | null) => void;
|
|
fitLayers: (targetLayers?: CanvasLayer[]) => void;
|
|
setActiveTool: Dispatch<SetStateAction<CanvasTool>>;
|
|
setActiveSidebarPanel: Dispatch<SetStateAction<SidebarPanel | null>>;
|
|
rememberImageModel: (imageModel: string) => void;
|
|
projectId?: string | null;
|
|
assetFolderId?: string | null;
|
|
upsertGeneratedAsset?: (asset: EditorAssetSnapshot) => void;
|
|
applyProjectSnapshot?: (project: EditorProjectSnapshot) => void;
|
|
onWalletBalanceMayHaveChanged?: () => void;
|
|
};
|
|
|
|
async function normalizeImageGenerationReferenceImages(
|
|
input: Parameters<typeof generateEditorImage>[0],
|
|
) {
|
|
if (!input.referenceImageSrcs?.length) {
|
|
return input;
|
|
}
|
|
|
|
return {
|
|
...input,
|
|
referenceImageSrcs: await Promise.all(
|
|
input.referenceImageSrcs.map((referenceImageSrc) =>
|
|
resolveEditorImageReferenceDataUrlForGeneration(referenceImageSrc),
|
|
),
|
|
),
|
|
};
|
|
}
|
|
|
|
async function normalizeIconSpritesheetReferenceImage(
|
|
input: Parameters<typeof generateEditorIconSpritesheet>[0],
|
|
) {
|
|
return {
|
|
...input,
|
|
referenceImageSrc: await resolveEditorImageReferenceDataUrlForGeneration(
|
|
input.referenceImageSrc,
|
|
),
|
|
};
|
|
}
|
|
|
|
function delay(ms: number) {
|
|
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
|
}
|
|
|
|
function queuedStateFromResponse(
|
|
response: { queueState?: ExternalGenerationJobStatusRecord | null } | null,
|
|
) {
|
|
return response?.queueState ?? null;
|
|
}
|
|
|
|
function notifyWalletBalanceMayHaveChanged(callback?: () => void) {
|
|
callback?.();
|
|
}
|
|
|
|
async function runEditorGenerationWithWalletRefresh<T>(
|
|
operation: Promise<T>,
|
|
onWalletBalanceMayHaveChanged?: () => void,
|
|
): Promise<T> {
|
|
try {
|
|
const response = await operation;
|
|
if (
|
|
!queuedStateFromResponse(
|
|
response as { queueState?: ExternalGenerationJobStatusRecord | null },
|
|
)
|
|
) {
|
|
notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged);
|
|
}
|
|
return response;
|
|
} catch (error) {
|
|
notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async function waitForEditorGenerationQueue(
|
|
queueState: ExternalGenerationJobStatusRecord,
|
|
): Promise<ExternalGenerationJobStatusRecord | null> {
|
|
const startedAt = Date.now();
|
|
let current = queueState;
|
|
while (current.status === 'queued' || current.status === 'running') {
|
|
if (Date.now() - startedAt > EDITOR_GENERATION_QUEUE_TIMEOUT_MS) {
|
|
return null;
|
|
}
|
|
await delay(EDITOR_GENERATION_QUEUE_POLL_INTERVAL_MS);
|
|
current = (
|
|
await getExternalGenerationJobStatus(current.operationId)
|
|
).job;
|
|
}
|
|
if (current.status === 'failed') {
|
|
throw new Error(current.error?.trim() || '生成任务失败');
|
|
}
|
|
return current;
|
|
}
|
|
|
|
async function applyQueuedEditorGenerationProject(
|
|
response: { queueState?: ExternalGenerationJobStatusRecord | null },
|
|
projectId: string | null | undefined,
|
|
applyProjectSnapshot: ((project: EditorProjectSnapshot) => void) | undefined,
|
|
onWalletBalanceMayHaveChanged?: () => void,
|
|
) {
|
|
const queueState = queuedStateFromResponse(response);
|
|
if (!queueState) {
|
|
return false;
|
|
}
|
|
let terminalState: ExternalGenerationJobStatusRecord | null = null;
|
|
try {
|
|
terminalState = await waitForEditorGenerationQueue(queueState);
|
|
} finally {
|
|
notifyWalletBalanceMayHaveChanged(onWalletBalanceMayHaveChanged);
|
|
}
|
|
if (!terminalState) {
|
|
return true;
|
|
}
|
|
if (projectId && applyProjectSnapshot) {
|
|
applyProjectSnapshot(await loadEditorProject(projectId));
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function positiveCanvasSize(value: number | null | undefined, fallback = 1) {
|
|
const resolved = Number.isFinite(value) && (value ?? 0) > 0 ? value : fallback;
|
|
return Math.max(1, Math.round(resolved ?? 1));
|
|
}
|
|
|
|
function buildRightSideCanvasCompletionPlaceholder(
|
|
sourceLayer: CanvasLayer,
|
|
size?: {
|
|
width?: number;
|
|
height?: number;
|
|
originalWidth?: number;
|
|
originalHeight?: number;
|
|
},
|
|
): EditorCanvasGenerationCompletionInput['placeholder'] {
|
|
const originalWidth = positiveCanvasSize(
|
|
size?.originalWidth ?? size?.width,
|
|
sourceLayer.originalWidth || sourceLayer.width || 1,
|
|
);
|
|
const originalHeight = positiveCanvasSize(
|
|
size?.originalHeight ?? size?.height,
|
|
sourceLayer.originalHeight || sourceLayer.height || 1,
|
|
);
|
|
const width = positiveCanvasSize(size?.width, originalWidth);
|
|
const height = positiveCanvasSize(size?.height, originalHeight);
|
|
return {
|
|
x: sourceLayer.x + sourceLayer.width + 32,
|
|
y: sourceLayer.y,
|
|
width,
|
|
height,
|
|
originalWidth,
|
|
originalHeight,
|
|
};
|
|
}
|
|
|
|
function isVideoQuickEditSource(layer: CanvasLayer) {
|
|
return layer.mediaType === 'video' || layer.assetKind === 'video';
|
|
}
|
|
|
|
function isCharacterAnimationQuickEditSource(layer: CanvasLayer) {
|
|
return layer.assetKind === 'character-animation';
|
|
}
|
|
|
|
export function useImageCanvasGenerationSubmissionWorkflow({
|
|
layers,
|
|
canvasSize,
|
|
viewport,
|
|
layerCounterRef,
|
|
quickEditPanel,
|
|
quickEditSourceLayer,
|
|
quickEditSelectionState,
|
|
canvasGenerationDialogs,
|
|
setQuickEditPanel,
|
|
characterAnimationPanel,
|
|
characterAnimationDialog,
|
|
characterAnimationSourceLayer,
|
|
setCharacterAnimationPanel,
|
|
setGenerateDialog,
|
|
openCanvasGenerationDialog,
|
|
updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
getGeneratingDialogPlaceholder,
|
|
appendCanvasLayersWithResources,
|
|
selectSingleLayer,
|
|
fitLayers,
|
|
setActiveTool,
|
|
setActiveSidebarPanel,
|
|
rememberImageModel,
|
|
projectId,
|
|
assetFolderId,
|
|
upsertGeneratedAsset,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
}: GenerationSubmissionWorkflowOptions) {
|
|
const addGeneratedLayersToCanvas = useCallback(
|
|
(nextLayers: CanvasLayer[]) => {
|
|
appendCanvasLayersWithResources(nextLayers);
|
|
nextLayers.forEach((layer) => {
|
|
const asset = layer.generatedAssetSnapshot;
|
|
if (asset) {
|
|
upsertGeneratedAsset?.(asset);
|
|
}
|
|
});
|
|
},
|
|
[appendCanvasLayersWithResources, upsertGeneratedAsset],
|
|
);
|
|
|
|
const addGeneratedResultLayer = useCallback(
|
|
(
|
|
generated: Parameters<typeof createGeneratedResultLayer>[0]['generated'],
|
|
options: {
|
|
sourceLayer?: CanvasLayer;
|
|
frame?: GenerateDialogState['placeholder'];
|
|
assetKind?: CanvasLayer['assetKind'];
|
|
title?: string;
|
|
dialogId?: string;
|
|
generationInputs?: CanvasGenerationInputs;
|
|
} = {},
|
|
) => {
|
|
if (
|
|
options.dialogId &&
|
|
!hasCanvasGenerationDialogById(options.dialogId)
|
|
) {
|
|
return;
|
|
}
|
|
layerCounterRef.current += 1;
|
|
const generatedIndex = layerCounterRef.current;
|
|
const nextLayer = createGeneratedResultLayer({
|
|
generated,
|
|
generatedIndex,
|
|
canvasSize,
|
|
viewport,
|
|
sourceLayer: options.sourceLayer,
|
|
frame: options.frame,
|
|
assetKind: options.assetKind,
|
|
title: options.title,
|
|
generationInputs: options.generationInputs,
|
|
});
|
|
|
|
addGeneratedLayersToCanvas([nextLayer]);
|
|
selectSingleLayer(nextLayer.id);
|
|
setActiveSidebarPanel('layers');
|
|
if (options.sourceLayer) {
|
|
setGenerateDialog(null);
|
|
setActiveTool('select');
|
|
} else if (options.dialogId) {
|
|
updateCanvasGenerationDialogById(options.dialogId, (currentDialog) => ({
|
|
...currentDialog,
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
generatedLayerId: nextLayer.id,
|
|
errorMessage: undefined,
|
|
}));
|
|
}
|
|
if (options.sourceLayer) {
|
|
fitLayers([options.sourceLayer, nextLayer]);
|
|
}
|
|
},
|
|
[
|
|
addGeneratedLayersToCanvas,
|
|
canvasSize,
|
|
fitLayers,
|
|
layerCounterRef,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
setGenerateDialog,
|
|
updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
const addQuickEditResultLayer = useCallback(
|
|
(
|
|
generated: Parameters<typeof createQuickEditResultLayer>[0]['generated'],
|
|
sourceLayer: CanvasLayer,
|
|
generationInputs: CanvasGenerationInputs,
|
|
mode: QuickEditPanelState['mode'] = 'quick-edit',
|
|
options: {
|
|
frame?: GenerateDialogState['placeholder'];
|
|
dialogId?: string;
|
|
} = {},
|
|
) => {
|
|
if (
|
|
options.dialogId &&
|
|
!hasCanvasGenerationDialogById(options.dialogId)
|
|
) {
|
|
return;
|
|
}
|
|
layerCounterRef.current += 1;
|
|
const generatedIndex = layerCounterRef.current;
|
|
const nextLayer = createQuickEditResultLayer({
|
|
generated,
|
|
generatedIndex,
|
|
sourceLayer,
|
|
generationInputs,
|
|
mode,
|
|
frame: options.frame,
|
|
});
|
|
|
|
addGeneratedLayersToCanvas([nextLayer]);
|
|
selectSingleLayer(mode === 'redraw' ? sourceLayer.id : nextLayer.id);
|
|
setActiveSidebarPanel('layers');
|
|
if (mode === 'redraw') {
|
|
setQuickEditPanel((currentPanel) =>
|
|
currentPanel?.sourceLayerId === sourceLayer.id
|
|
? {
|
|
...currentPanel,
|
|
mode: 'redraw',
|
|
status: 'idle',
|
|
errorMessage: undefined,
|
|
}
|
|
: currentPanel,
|
|
);
|
|
} else {
|
|
setQuickEditPanel(null);
|
|
setActiveTool('select');
|
|
if (options.dialogId) {
|
|
updateCanvasGenerationDialogById(
|
|
options.dialogId,
|
|
(currentDialog) => ({
|
|
...currentDialog,
|
|
status: 'idle',
|
|
composerOpen: false,
|
|
generatedLayerId: nextLayer.id,
|
|
errorMessage: undefined,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
fitLayers([sourceLayer, nextLayer]);
|
|
},
|
|
[
|
|
addGeneratedLayersToCanvas,
|
|
fitLayers,
|
|
layerCounterRef,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
setQuickEditPanel,
|
|
updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
],
|
|
);
|
|
|
|
const addIconSpritesheetResultLayers = useCallback(
|
|
(
|
|
generated: Parameters<
|
|
typeof createIconSpritesheetResultLayers
|
|
>[0]['generated'],
|
|
iconResults: Parameters<
|
|
typeof createIconSpritesheetResultLayers
|
|
>[0]['iconResults'],
|
|
generationInputs: CanvasGenerationInputs,
|
|
frame?: GenerateDialogState['placeholder'],
|
|
dialogId?: string,
|
|
options: {
|
|
spritesheetTitle?: string;
|
|
} = {},
|
|
) => {
|
|
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
|
|
return [];
|
|
}
|
|
const startIndex = layerCounterRef.current + 1;
|
|
const nextLayers = createIconSpritesheetResultLayers({
|
|
generated,
|
|
iconResults,
|
|
startIndex,
|
|
canvasSize,
|
|
viewport,
|
|
generationInputs,
|
|
frame,
|
|
spritesheetTitle: options.spritesheetTitle,
|
|
});
|
|
|
|
if (!nextLayers.length) {
|
|
return [];
|
|
}
|
|
layerCounterRef.current += nextLayers.length;
|
|
addGeneratedLayersToCanvas(nextLayers);
|
|
selectSingleLayer(nextLayers[0]?.id ?? null);
|
|
setActiveSidebarPanel('layers');
|
|
if (dialogId) {
|
|
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
|
|
...currentDialog,
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
generatedLayerId: nextLayers[0]?.id,
|
|
errorMessage: undefined,
|
|
}));
|
|
}
|
|
setActiveTool('select');
|
|
return nextLayers;
|
|
},
|
|
[
|
|
addGeneratedLayersToCanvas,
|
|
canvasSize,
|
|
layerCounterRef,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
const addVideoResultLayer = useCallback(
|
|
(
|
|
generated: Parameters<typeof createVideoResultLayer>[0]['generated'],
|
|
title: string,
|
|
generationInputs: CanvasGenerationInputs,
|
|
frame?: GenerateDialogState['placeholder'],
|
|
dialogId?: string,
|
|
options: {
|
|
sourceLayer?: CanvasLayer;
|
|
assetKind?: CanvasLayer['assetKind'];
|
|
} = {},
|
|
) => {
|
|
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
|
|
return;
|
|
}
|
|
layerCounterRef.current += 1;
|
|
const generatedIndex = layerCounterRef.current;
|
|
const nextLayer = createVideoResultLayer({
|
|
generated,
|
|
generatedIndex,
|
|
title,
|
|
canvasSize,
|
|
viewport,
|
|
generationInputs,
|
|
frame,
|
|
sourceLayer: options.sourceLayer,
|
|
assetKind: options.assetKind,
|
|
});
|
|
|
|
addGeneratedLayersToCanvas([nextLayer]);
|
|
selectSingleLayer(nextLayer.id);
|
|
setActiveSidebarPanel('layers');
|
|
if (dialogId) {
|
|
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
|
|
...currentDialog,
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
generatedLayerId: nextLayer.id,
|
|
errorMessage: undefined,
|
|
}));
|
|
}
|
|
setActiveTool('select');
|
|
},
|
|
[
|
|
addGeneratedLayersToCanvas,
|
|
canvasSize,
|
|
layerCounterRef,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
const addAudioResultLayer = useCallback(
|
|
(
|
|
generated: Parameters<typeof createAudioResultLayer>[0]['generated'],
|
|
title: string,
|
|
generationInputs: CanvasGenerationInputs,
|
|
frame?: GenerateDialogState['placeholder'],
|
|
dialogId?: string,
|
|
) => {
|
|
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
|
|
return;
|
|
}
|
|
layerCounterRef.current += 1;
|
|
const generatedIndex = layerCounterRef.current;
|
|
const nextLayer = createAudioResultLayer({
|
|
generated,
|
|
generatedIndex,
|
|
title,
|
|
canvasSize,
|
|
viewport,
|
|
generationInputs,
|
|
frame,
|
|
});
|
|
|
|
addGeneratedLayersToCanvas([nextLayer]);
|
|
selectSingleLayer(nextLayer.id);
|
|
setActiveSidebarPanel('layers');
|
|
if (dialogId) {
|
|
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
|
|
...currentDialog,
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
generatedLayerId: nextLayer.id,
|
|
errorMessage: undefined,
|
|
}));
|
|
}
|
|
setActiveTool('select');
|
|
},
|
|
[
|
|
addGeneratedLayersToCanvas,
|
|
canvasSize,
|
|
layerCounterRef,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
const addCharacterAnimationResultLayer = useCallback(
|
|
(
|
|
generated: Parameters<
|
|
typeof createCharacterAnimationResultLayer
|
|
>[0]['generated'],
|
|
title: string,
|
|
generationInputs: CanvasGenerationInputs,
|
|
frame?: GenerateDialogState['placeholder'],
|
|
dialogId?: string,
|
|
options: {
|
|
sourceLayer?: CanvasLayer;
|
|
} = {},
|
|
) => {
|
|
if (dialogId && !hasCanvasGenerationDialogById(dialogId)) {
|
|
return;
|
|
}
|
|
layerCounterRef.current += 1;
|
|
const generatedIndex = layerCounterRef.current;
|
|
const nextLayer = createCharacterAnimationResultLayer({
|
|
generated,
|
|
generatedIndex,
|
|
title,
|
|
canvasSize,
|
|
viewport,
|
|
generationInputs,
|
|
frame,
|
|
sourceLayer: options.sourceLayer,
|
|
});
|
|
|
|
if (!nextLayer) {
|
|
return;
|
|
}
|
|
addGeneratedLayersToCanvas([nextLayer]);
|
|
selectSingleLayer(nextLayer.id);
|
|
setActiveSidebarPanel('layers');
|
|
if (dialogId) {
|
|
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
|
|
...currentDialog,
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
generatedLayerId: nextLayer.id,
|
|
characterAnimationResult: generated,
|
|
errorMessage: undefined,
|
|
}));
|
|
}
|
|
setActiveTool('select');
|
|
},
|
|
[
|
|
addGeneratedLayersToCanvas,
|
|
canvasSize,
|
|
layerCounterRef,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
updateCanvasGenerationDialogById,
|
|
hasCanvasGenerationDialogById,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
const extractUiDesignAssets = useCallback(
|
|
async (
|
|
sourceLayer: CanvasLayer,
|
|
options: UiDesignAssetExtractionOptions = {},
|
|
) => {
|
|
if (sourceLayer.assetKind !== 'ui-design') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let sourceImageSrc = await resolveEditorImageReferenceDataUrl(
|
|
sourceLayer.objectKey?.trim() || sourceLayer.src,
|
|
);
|
|
if (options.marks?.length) {
|
|
sourceImageSrc = await renderUiDesignAssetExtractionMarkedImage({
|
|
source: sourceImageSrc,
|
|
marks: options.marks,
|
|
});
|
|
}
|
|
const generationInputs =
|
|
buildUiDesignAssetExtractionGenerationInputs(sourceLayer);
|
|
const extractionPlan = resolveUiAssetExtractionGenerationPlan(
|
|
options.marks?.length ?? 0,
|
|
);
|
|
const extractionFrame = {
|
|
x: sourceLayer.x + sourceLayer.width + 32,
|
|
y: sourceLayer.y,
|
|
width: ICON_FRAME_DISPLAY_SIZE.width,
|
|
height: ICON_FRAME_DISPLAY_SIZE.height,
|
|
originalWidth: ICON_FRAME_ORIGINAL_SIZE.width,
|
|
originalHeight: ICON_FRAME_ORIGINAL_SIZE.height,
|
|
};
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
extractEditorUiDesignAssets({
|
|
sourceImageSrc,
|
|
model: normalizeEditorImageModel(
|
|
options.model ?? DEFAULT_IMAGE_MODEL,
|
|
),
|
|
projectId,
|
|
generationInputs,
|
|
assetFolderId,
|
|
spritesheetLabel: `${sourceLayer.title} 素材图集`,
|
|
aspectRatio: extractionPlan.aspectRatio,
|
|
imageSize: extractionPlan.imageSize,
|
|
...(projectId
|
|
? {
|
|
canvasCompletion: {
|
|
title: `${sourceLayer.title} 素材图集`,
|
|
placeholder: extractionFrame,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
return;
|
|
}
|
|
const nextLayers = addIconSpritesheetResultLayers(
|
|
generated,
|
|
generated.iconImageSrcs ?? [],
|
|
generationInputs,
|
|
extractionFrame,
|
|
undefined,
|
|
{
|
|
spritesheetTitle: `${sourceLayer.title} 素材图集`,
|
|
},
|
|
);
|
|
if (nextLayers.length) {
|
|
fitLayers([sourceLayer, ...nextLayers]);
|
|
}
|
|
} catch (error) {
|
|
if (options.suppressAlert) {
|
|
throw error;
|
|
}
|
|
window.alert(
|
|
error instanceof Error && error.message.trim()
|
|
? error.message
|
|
: '提取素材失败',
|
|
);
|
|
}
|
|
},
|
|
[
|
|
addIconSpritesheetResultLayers,
|
|
applyProjectSnapshot,
|
|
fitLayers,
|
|
projectId,
|
|
assetFolderId,
|
|
onWalletBalanceMayHaveChanged,
|
|
],
|
|
);
|
|
|
|
const submitIconSpritesheetGeneration = useCallback(
|
|
async (dialog: GenerateDialogState) => {
|
|
if (dialog.mode !== 'icon') {
|
|
return;
|
|
}
|
|
const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
|
|
const setSubmittingIconDialog = (
|
|
nextDialog: CanvasGenerationDialogState,
|
|
) => {
|
|
updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog);
|
|
};
|
|
const submissionPlan =
|
|
buildIconSpritesheetGenerationSubmissionPlan(dialog);
|
|
if (submissionPlan.ok === false) {
|
|
if (canvasDialog) {
|
|
setSubmittingIconDialog({
|
|
...canvasDialog,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage: submissionPlan.errorMessage,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!canvasDialog) {
|
|
return;
|
|
}
|
|
|
|
setSubmittingIconDialog({
|
|
...canvasDialog,
|
|
iconDescriptions: submissionPlan.iconDescriptions,
|
|
status: 'generating',
|
|
composerOpen: false,
|
|
errorMessage: undefined,
|
|
});
|
|
|
|
try {
|
|
const iconGenerationInput =
|
|
await normalizeIconSpritesheetReferenceImage(submissionPlan.input);
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorIconSpritesheet({
|
|
...iconGenerationInput,
|
|
projectId,
|
|
generationInputs: submissionPlan.generationInputs,
|
|
assetFolderId,
|
|
...(projectId && canvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog.id,
|
|
title: '图标素材图集',
|
|
placeholder: canvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
rememberImageModel(submissionPlan.rememberImageModel);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
return;
|
|
}
|
|
addIconSpritesheetResultLayers(
|
|
generated,
|
|
generated.iconImageSrcs ?? [],
|
|
submissionPlan.generationInputs,
|
|
canvasCompletionPlaceholder,
|
|
canvasDialog.id,
|
|
);
|
|
} catch (error) {
|
|
setSubmittingIconDialog({
|
|
...canvasDialog,
|
|
iconDescriptions: submissionPlan.iconDescriptions,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage: resolveImageGenerationErrorMessage(error),
|
|
});
|
|
}
|
|
},
|
|
[
|
|
addIconSpritesheetResultLayers,
|
|
applyProjectSnapshot,
|
|
getGeneratingDialogPlaceholder,
|
|
rememberImageModel,
|
|
projectId,
|
|
assetFolderId,
|
|
updateCanvasGenerationDialogById,
|
|
onWalletBalanceMayHaveChanged,
|
|
],
|
|
);
|
|
|
|
const submitQuickEdit = useCallback(async () => {
|
|
if (!quickEditPanel || !quickEditSourceLayer) {
|
|
return;
|
|
}
|
|
|
|
const panelMode = quickEditPanel.mode ?? 'quick-edit';
|
|
const quickEditReferences =
|
|
panelMode === 'redraw' ? [] : (quickEditPanel.quickEditReferences ?? []);
|
|
const sourceReferenceIndex = quickEditReferences.length + 1;
|
|
const quickEditAspectRatio =
|
|
quickEditPanel.aspectRatio ??
|
|
inferEditorImageAspectRatio(
|
|
quickEditSourceLayer.originalWidth,
|
|
quickEditSourceLayer.originalHeight,
|
|
);
|
|
const quickEditImageSize =
|
|
quickEditPanel.imageSize ??
|
|
inferEditorImageSizeLabel(
|
|
quickEditSourceLayer.originalWidth,
|
|
quickEditSourceLayer.originalHeight,
|
|
);
|
|
const quickEditOutputSize = resolveEditorImageGenerationPixelSize({
|
|
model: quickEditPanel.model,
|
|
aspectRatio: quickEditAspectRatio,
|
|
imageSize: quickEditImageSize,
|
|
});
|
|
const quickEditSize = `${quickEditOutputSize.width}x${quickEditOutputSize.height}`;
|
|
const basePrompt =
|
|
quickEditPanel.prompt.trim() ||
|
|
(panelMode === 'redraw' ? '重绘图片' : '快速编辑图片');
|
|
const normalizedPrompt =
|
|
panelMode === 'redraw'
|
|
? basePrompt
|
|
: normalizeQuickEditPromptSourceReference(
|
|
basePrompt,
|
|
sourceReferenceIndex,
|
|
);
|
|
let quickEditDialogId: string | undefined;
|
|
if (panelMode === 'quick-edit') {
|
|
const quickEditDraft = createQuickEditGenerationDialogDraft({
|
|
sourceLayer: quickEditSourceLayer,
|
|
prompt: normalizedPrompt,
|
|
status: 'generating',
|
|
references: quickEditReferences,
|
|
model: quickEditPanel.model,
|
|
aspectRatio: quickEditAspectRatio,
|
|
imageSize: quickEditImageSize,
|
|
frame: {
|
|
width: quickEditOutputSize.width,
|
|
height: quickEditOutputSize.height,
|
|
},
|
|
});
|
|
const quickEditPlacement = quickEditDraft.placeholder
|
|
? chooseGenerationPlacement({
|
|
canvasSize,
|
|
viewport,
|
|
frame: quickEditDraft.placeholder,
|
|
layers,
|
|
generationDialogs: canvasGenerationDialogs,
|
|
})
|
|
: quickEditDraft.placeholder;
|
|
quickEditDialogId = openCanvasGenerationDialog({
|
|
...quickEditDraft,
|
|
// 中文注释:快速编辑生成结果复用新建图片的避让落点,避免覆盖已有素材。
|
|
placeholder: quickEditPlacement,
|
|
});
|
|
setQuickEditPanel(null);
|
|
selectSingleLayer(null);
|
|
} else {
|
|
setQuickEditPanel({
|
|
...quickEditPanel,
|
|
aspectRatio: quickEditAspectRatio,
|
|
imageSize: quickEditImageSize,
|
|
size: quickEditSize,
|
|
prompt: normalizedPrompt,
|
|
status: 'generating',
|
|
errorMessage: undefined,
|
|
});
|
|
}
|
|
|
|
try {
|
|
const extraReferenceImageSrcs = await Promise.all(
|
|
quickEditReferences.map((reference) =>
|
|
resolveEditorImageReferenceDataUrl(
|
|
reference.objectKey?.trim() || reference.src,
|
|
),
|
|
),
|
|
);
|
|
const quickEditFrame = quickEditDialogId
|
|
? getGeneratingDialogPlaceholder({
|
|
id: quickEditDialogId,
|
|
mode: 'quick-edit',
|
|
prompt: normalizedPrompt,
|
|
status: 'generating',
|
|
})
|
|
: undefined;
|
|
const quickEditCanvasCompletionPlaceholder =
|
|
quickEditFrame ??
|
|
buildRightSideCanvasCompletionPlaceholder(quickEditSourceLayer, {
|
|
width: quickEditOutputSize.width,
|
|
height: quickEditOutputSize.height,
|
|
originalWidth: quickEditOutputSize.width,
|
|
originalHeight: quickEditOutputSize.height,
|
|
});
|
|
if (
|
|
panelMode === 'quick-edit' &&
|
|
isCharacterAnimationQuickEditSource(quickEditSourceLayer)
|
|
) {
|
|
const animationResolution = '480p' as const;
|
|
const animationDurationSeconds = 4 as const;
|
|
const animationFrameCount = 32 as const;
|
|
const generationInputs = buildCharacterAnimationGenerationInputs(
|
|
normalizedPrompt,
|
|
quickEditSourceLayer,
|
|
);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorCharacterAnimation({
|
|
sourceLayerId: quickEditSourceLayer.id,
|
|
sourceImageSrc:
|
|
resolveCharacterAnimationSourceImageSrc(quickEditSourceLayer),
|
|
sourceWidth: quickEditSourceLayer.originalWidth,
|
|
sourceHeight: quickEditSourceLayer.originalHeight,
|
|
promptText: normalizedPrompt,
|
|
resolution: animationResolution,
|
|
ratio: 'same',
|
|
frameCount: animationFrameCount,
|
|
durationSeconds: animationDurationSeconds,
|
|
model: CHARACTER_ANIMATION_MODEL,
|
|
projectId,
|
|
generationInputs,
|
|
sourceResourceId: quickEditSourceLayer.resourceId,
|
|
...(projectId && quickEditCanvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: quickEditDialogId,
|
|
title: `${quickEditSourceLayer.title} 快速编辑`,
|
|
placeholder: quickEditCanvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
return;
|
|
}
|
|
addCharacterAnimationResultLayer(
|
|
generated,
|
|
`${quickEditSourceLayer.title} 快速编辑`,
|
|
generationInputs,
|
|
quickEditFrame,
|
|
quickEditDialogId,
|
|
{
|
|
sourceLayer: quickEditSourceLayer,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
if (
|
|
panelMode === 'quick-edit' &&
|
|
isVideoQuickEditSource(quickEditSourceLayer)
|
|
) {
|
|
const videoModel = DEFAULT_VIDEO_MODEL;
|
|
const videoResolution = '480p' as const;
|
|
const videoDurationSeconds = 4 as const;
|
|
const sourceVideoSrc =
|
|
quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src;
|
|
const generationInputs = buildVideoGenerationInputs(
|
|
normalizedPrompt,
|
|
quickEditReferences,
|
|
);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorVideo({
|
|
prompt: normalizedPrompt,
|
|
model: videoModel,
|
|
aspectRatio: '16:9',
|
|
durationSeconds: videoDurationSeconds,
|
|
resolution: videoResolution,
|
|
mode: 'std',
|
|
sound: 'off',
|
|
...(extraReferenceImageSrcs.length
|
|
? { referenceImageSrcs: extraReferenceImageSrcs }
|
|
: {}),
|
|
referenceVideoSrcs: [sourceVideoSrc],
|
|
projectId,
|
|
generationInputs,
|
|
sourceResourceId: quickEditSourceLayer.resourceId,
|
|
assetKind: quickEditSourceLayer.assetKind,
|
|
...(projectId && quickEditCanvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: quickEditDialogId,
|
|
title: `${quickEditSourceLayer.title} 快速编辑`,
|
|
placeholder: quickEditCanvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
return;
|
|
}
|
|
addVideoResultLayer(
|
|
generated,
|
|
`${quickEditSourceLayer.title} 快速编辑`,
|
|
generationInputs,
|
|
quickEditFrame,
|
|
quickEditDialogId,
|
|
{
|
|
sourceLayer: quickEditSourceLayer,
|
|
assetKind: quickEditSourceLayer.assetKind,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
let sourceReferenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
|
quickEditSourceLayer.objectKey?.trim() || quickEditSourceLayer.src,
|
|
);
|
|
const quickEditSelectionMarks =
|
|
panelMode === 'quick-edit' &&
|
|
quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id
|
|
? quickEditSelectionState.marks
|
|
: [];
|
|
if (quickEditSelectionMarks.length > 0) {
|
|
sourceReferenceImageSrc =
|
|
await renderUiDesignAssetExtractionMarkedImage({
|
|
source: sourceReferenceImageSrc,
|
|
marks: quickEditSelectionMarks,
|
|
showIndexLabels: true,
|
|
});
|
|
}
|
|
const generationInputs =
|
|
panelMode === 'redraw'
|
|
? buildEditGenerationInputs(
|
|
'重绘提示词',
|
|
normalizedPrompt,
|
|
quickEditSourceLayer,
|
|
)
|
|
: buildQuickEditGenerationInputs(
|
|
'快速编辑提示词',
|
|
normalizedPrompt,
|
|
quickEditSourceLayer,
|
|
quickEditReferences,
|
|
);
|
|
const isCharacterRedraw =
|
|
panelMode === 'redraw' &&
|
|
quickEditSourceLayer.assetKind === 'character';
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorImage({
|
|
prompt: normalizedPrompt,
|
|
size: quickEditSize,
|
|
aspectRatio: quickEditAspectRatio,
|
|
imageSize: quickEditImageSize,
|
|
kind: isCharacterRedraw ? 'character' : 'quick-edit',
|
|
model: normalizeEditorImageModel(quickEditPanel.model),
|
|
referenceImageSrcs: [
|
|
...extraReferenceImageSrcs,
|
|
sourceReferenceImageSrc,
|
|
],
|
|
projectId,
|
|
assetKind: quickEditSourceLayer.assetKind,
|
|
generationInputs,
|
|
assetFolderId,
|
|
assetLabel: `${quickEditSourceLayer.title} ${
|
|
panelMode === 'redraw' ? '重绘' : '快速编辑'
|
|
}`,
|
|
sourceResourceId: quickEditSourceLayer.resourceId,
|
|
...(projectId && quickEditCanvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: quickEditDialogId,
|
|
title: `${quickEditSourceLayer.title} ${
|
|
panelMode === 'redraw' ? '重绘' : '快速编辑'
|
|
}`,
|
|
placeholder: quickEditCanvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
return;
|
|
}
|
|
addQuickEditResultLayer(
|
|
generated,
|
|
quickEditSourceLayer,
|
|
generationInputs,
|
|
panelMode,
|
|
quickEditDialogId
|
|
? {
|
|
frame: quickEditFrame,
|
|
dialogId: quickEditDialogId,
|
|
}
|
|
: {},
|
|
);
|
|
} catch (error) {
|
|
if (quickEditDialogId) {
|
|
updateCanvasGenerationDialogById(
|
|
quickEditDialogId,
|
|
(currentDialog) => ({
|
|
...currentDialog,
|
|
prompt: normalizedPrompt,
|
|
status: 'failed',
|
|
composerOpen: false,
|
|
errorMessage: resolveImageGenerationErrorMessage(error),
|
|
}),
|
|
);
|
|
}
|
|
setQuickEditPanel({
|
|
...quickEditPanel,
|
|
aspectRatio: quickEditAspectRatio,
|
|
imageSize: quickEditImageSize,
|
|
size: quickEditSize,
|
|
prompt: normalizedPrompt,
|
|
status: 'failed',
|
|
errorMessage: resolveImageGenerationErrorMessage(error),
|
|
});
|
|
}
|
|
}, [
|
|
addCharacterAnimationResultLayer,
|
|
addQuickEditResultLayer,
|
|
addVideoResultLayer,
|
|
applyProjectSnapshot,
|
|
canvasGenerationDialogs,
|
|
canvasSize,
|
|
getGeneratingDialogPlaceholder,
|
|
layers,
|
|
openCanvasGenerationDialog,
|
|
projectId,
|
|
assetFolderId,
|
|
quickEditPanel,
|
|
quickEditSourceLayer,
|
|
quickEditSelectionState,
|
|
selectSingleLayer,
|
|
setQuickEditPanel,
|
|
updateCanvasGenerationDialogById,
|
|
viewport,
|
|
onWalletBalanceMayHaveChanged,
|
|
]);
|
|
|
|
const submitImageGeneration = useCallback(
|
|
async (dialog: GenerateDialogState) => {
|
|
const normalizedPrompt =
|
|
dialog.prompt.trim() ||
|
|
(dialog.mode === 'edit'
|
|
? '修改当前图片'
|
|
: dialog.mode === 'audio-sound-effect'
|
|
? '游戏音效'
|
|
: dialog.mode === 'audio-background-music'
|
|
? '游戏背景音乐'
|
|
: 'AI 生成图片');
|
|
const canvasDialog = isCanvasGenerationDialog(dialog) ? dialog : null;
|
|
if (canvasDialog) {
|
|
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
|
|
...currentDialog,
|
|
prompt: normalizedPrompt,
|
|
status: 'generating',
|
|
composerOpen: false,
|
|
}));
|
|
} else {
|
|
setGenerateDialog({
|
|
...dialog,
|
|
prompt: normalizedPrompt,
|
|
status: 'generating',
|
|
composerOpen: dialog.mode === 'edit',
|
|
});
|
|
}
|
|
|
|
try {
|
|
const submissionPlan = buildImageGenerationSubmissionPlan({
|
|
dialog,
|
|
layers,
|
|
nextGeneratedIndex: layerCounterRef.current + 1,
|
|
});
|
|
if (submissionPlan.kind === 'edit') {
|
|
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
|
submissionPlan.sourceLayer.objectKey?.trim() ||
|
|
submissionPlan.sourceLayer.src,
|
|
);
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const editCanvasCompletionPlaceholder =
|
|
canvasCompletionPlaceholder ??
|
|
buildRightSideCanvasCompletionPlaceholder(
|
|
submissionPlan.sourceLayer,
|
|
{
|
|
width: 1024,
|
|
height: 1024,
|
|
originalWidth: 1024,
|
|
originalHeight: 1024,
|
|
},
|
|
);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
editEditorImage({
|
|
prompt: submissionPlan.normalizedPrompt,
|
|
sourceImageSrc: referenceImageSrc,
|
|
projectId,
|
|
assetKind: submissionPlan.sourceLayer.assetKind,
|
|
generationInputs: submissionPlan.generationInputs,
|
|
assetFolderId,
|
|
assetLabel: `${submissionPlan.sourceLayer.title} 修改结果`,
|
|
sourceResourceId: submissionPlan.sourceLayer.resourceId,
|
|
...(projectId && editCanvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog?.id,
|
|
title: `${submissionPlan.sourceLayer.title} 修改结果`,
|
|
placeholder: editCanvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
return;
|
|
}
|
|
addGeneratedResultLayer(generated, {
|
|
sourceLayer: submissionPlan.sourceLayer,
|
|
generationInputs: submissionPlan.generationInputs,
|
|
});
|
|
} else if (submissionPlan.kind === 'quick-edit') {
|
|
const imageGenerationInput =
|
|
await normalizeImageGenerationReferenceImages(submissionPlan.input);
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorImage({
|
|
...imageGenerationInput,
|
|
projectId,
|
|
assetKind: submissionPlan.result.assetKind,
|
|
generationInputs: submissionPlan.result.generationInputs,
|
|
assetFolderId,
|
|
assetLabel: submissionPlan.result.title,
|
|
sourceResourceId: submissionPlan.sourceLayer.resourceId,
|
|
...(projectId && canvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog?.id,
|
|
title: submissionPlan.result.title,
|
|
placeholder: canvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (submissionPlan.rememberImageModel) {
|
|
rememberImageModel(submissionPlan.rememberImageModel);
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
if (generated.asset) {
|
|
upsertGeneratedAsset?.(generated.asset);
|
|
}
|
|
return;
|
|
}
|
|
addQuickEditResultLayer(
|
|
generated,
|
|
submissionPlan.sourceLayer,
|
|
submissionPlan.result.generationInputs,
|
|
'quick-edit',
|
|
{
|
|
frame: canvasCompletionPlaceholder,
|
|
dialogId: canvasDialog?.id,
|
|
},
|
|
);
|
|
} else if (submissionPlan.kind === 'video') {
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorVideo({
|
|
...submissionPlan.input,
|
|
projectId,
|
|
generationInputs: submissionPlan.result.generationInputs,
|
|
...(projectId && canvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog?.id,
|
|
title: submissionPlan.result.title,
|
|
placeholder: canvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
return;
|
|
}
|
|
addVideoResultLayer(
|
|
generated,
|
|
submissionPlan.result.title,
|
|
submissionPlan.result.generationInputs,
|
|
canvasCompletionPlaceholder,
|
|
canvasDialog?.id,
|
|
);
|
|
} else if (submissionPlan.kind === 'audio') {
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
submissionPlan.audioKind === 'sound-effect'
|
|
? generateEditorSoundEffect({
|
|
...submissionPlan.input,
|
|
projectId,
|
|
generationInputs: submissionPlan.result.generationInputs,
|
|
...(projectId && canvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog?.id,
|
|
title: submissionPlan.result.title,
|
|
placeholder: canvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
})
|
|
: generateEditorBackgroundMusic({
|
|
...submissionPlan.input,
|
|
projectId,
|
|
generationInputs: submissionPlan.result.generationInputs,
|
|
...(projectId && canvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog?.id,
|
|
title: submissionPlan.result.title,
|
|
placeholder: canvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
return;
|
|
}
|
|
addAudioResultLayer(
|
|
generated,
|
|
submissionPlan.result.title,
|
|
submissionPlan.result.generationInputs,
|
|
canvasCompletionPlaceholder,
|
|
canvasDialog?.id,
|
|
);
|
|
} else {
|
|
const imageGenerationInput =
|
|
await normalizeImageGenerationReferenceImages(submissionPlan.input);
|
|
const resultTitle =
|
|
submissionPlan.result.title ??
|
|
`生成图片 ${layerCounterRef.current + 1}`;
|
|
const canvasCompletionPlaceholder =
|
|
getGeneratingDialogPlaceholder(dialog);
|
|
const generated = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorImage({
|
|
...imageGenerationInput,
|
|
projectId,
|
|
assetKind: submissionPlan.result.assetKind,
|
|
generationInputs: submissionPlan.result.generationInputs,
|
|
assetFolderId,
|
|
assetLabel: resultTitle,
|
|
...(projectId && canvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog?.id,
|
|
title: resultTitle,
|
|
placeholder: canvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
generated,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (submissionPlan.rememberImageModel) {
|
|
rememberImageModel(submissionPlan.rememberImageModel);
|
|
}
|
|
if (generated.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(generated.project);
|
|
if (generated.asset) {
|
|
upsertGeneratedAsset?.(generated.asset);
|
|
}
|
|
return;
|
|
}
|
|
if (canvasDialog && projectId) {
|
|
updateCanvasGenerationDialogById(
|
|
canvasDialog.id,
|
|
(currentDialog) =>
|
|
currentDialog.generatedLayerId
|
|
? currentDialog
|
|
: {
|
|
...currentDialog,
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
errorMessage: undefined,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
addGeneratedResultLayer(generated, {
|
|
frame: canvasCompletionPlaceholder,
|
|
assetKind: submissionPlan.result.assetKind,
|
|
title: resultTitle,
|
|
dialogId: canvasDialog?.id,
|
|
generationInputs: submissionPlan.result.generationInputs,
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (canvasDialog) {
|
|
updateCanvasGenerationDialogById(canvasDialog.id, () => ({
|
|
...canvasDialog,
|
|
prompt: normalizedPrompt,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage: resolveImageGenerationErrorMessage(error),
|
|
}));
|
|
} else {
|
|
setGenerateDialog({
|
|
...dialog,
|
|
prompt: normalizedPrompt,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage: resolveImageGenerationErrorMessage(error),
|
|
});
|
|
}
|
|
}
|
|
},
|
|
[
|
|
addGeneratedResultLayer,
|
|
addAudioResultLayer,
|
|
addVideoResultLayer,
|
|
getGeneratingDialogPlaceholder,
|
|
layerCounterRef,
|
|
layers,
|
|
rememberImageModel,
|
|
projectId,
|
|
assetFolderId,
|
|
applyProjectSnapshot,
|
|
setGenerateDialog,
|
|
updateCanvasGenerationDialogById,
|
|
upsertGeneratedAsset,
|
|
onWalletBalanceMayHaveChanged,
|
|
],
|
|
);
|
|
|
|
const submitCharacterAnimation = useCallback(async () => {
|
|
if (!characterAnimationPanel || !characterAnimationSourceLayer) {
|
|
return;
|
|
}
|
|
if (
|
|
characterAnimationPanel.status === 'generating' ||
|
|
(characterAnimationDialog?.mode === 'character-animation' &&
|
|
characterAnimationDialog.status === 'generating')
|
|
) {
|
|
return;
|
|
}
|
|
const submissionPlan = buildCharacterAnimationSubmissionPlan({
|
|
panel: characterAnimationPanel,
|
|
sourceLayer: characterAnimationSourceLayer,
|
|
});
|
|
const canvasDialog = characterAnimationDialog;
|
|
if (canvasDialog?.mode === 'character-animation') {
|
|
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
|
|
...currentDialog,
|
|
prompt: submissionPlan.promptText,
|
|
characterAnimationResolution: characterAnimationPanel.resolution,
|
|
characterAnimationRatio: characterAnimationPanel.ratio,
|
|
characterAnimationFrameCount: characterAnimationPanel.frameCount,
|
|
characterAnimationDurationSeconds:
|
|
characterAnimationPanel.durationSeconds,
|
|
characterAnimationResult: undefined,
|
|
status: 'generating',
|
|
composerOpen: false,
|
|
errorMessage: undefined,
|
|
}));
|
|
}
|
|
const nextPanel = {
|
|
...characterAnimationPanel,
|
|
promptText: submissionPlan.promptText,
|
|
status: 'generating' as const,
|
|
errorMessage: undefined,
|
|
result: undefined,
|
|
};
|
|
if (!canvasDialog) {
|
|
setCharacterAnimationPanel(nextPanel);
|
|
}
|
|
|
|
try {
|
|
const generationInputs = buildCharacterAnimationGenerationInputs(
|
|
submissionPlan.promptText,
|
|
characterAnimationSourceLayer,
|
|
);
|
|
const canvasCompletionPlaceholder = canvasDialog
|
|
? getGeneratingDialogPlaceholder(canvasDialog)
|
|
: undefined;
|
|
const result = await runEditorGenerationWithWalletRefresh(
|
|
generateEditorCharacterAnimation({
|
|
...submissionPlan.input,
|
|
projectId,
|
|
generationInputs,
|
|
sourceResourceId: characterAnimationSourceLayer.resourceId,
|
|
...(canvasDialog && projectId && canvasCompletionPlaceholder
|
|
? {
|
|
canvasCompletion: {
|
|
dialogId: canvasDialog.id,
|
|
title: '角色动作',
|
|
placeholder: canvasCompletionPlaceholder,
|
|
},
|
|
}
|
|
: {}),
|
|
}),
|
|
onWalletBalanceMayHaveChanged,
|
|
);
|
|
if (
|
|
await applyQueuedEditorGenerationProject(
|
|
result,
|
|
projectId,
|
|
applyProjectSnapshot,
|
|
onWalletBalanceMayHaveChanged,
|
|
)
|
|
) {
|
|
return;
|
|
}
|
|
if (result.project && applyProjectSnapshot) {
|
|
applyProjectSnapshot(result.project);
|
|
return;
|
|
}
|
|
if (canvasDialog) {
|
|
addCharacterAnimationResultLayer(
|
|
result,
|
|
'角色动作',
|
|
generationInputs,
|
|
canvasCompletionPlaceholder,
|
|
canvasDialog.id,
|
|
{
|
|
sourceLayer: characterAnimationSourceLayer,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
setCharacterAnimationPanel((currentPanel) =>
|
|
currentPanel
|
|
? {
|
|
...currentPanel,
|
|
status: 'completed',
|
|
result,
|
|
}
|
|
: currentPanel,
|
|
);
|
|
} catch (error) {
|
|
if (canvasDialog) {
|
|
updateCanvasGenerationDialogById(canvasDialog.id, (currentDialog) => ({
|
|
...currentDialog,
|
|
prompt: submissionPlan.promptText,
|
|
characterAnimationResolution: characterAnimationPanel.resolution,
|
|
characterAnimationRatio: characterAnimationPanel.ratio,
|
|
characterAnimationFrameCount: characterAnimationPanel.frameCount,
|
|
characterAnimationDurationSeconds:
|
|
characterAnimationPanel.durationSeconds,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage: resolveImageGenerationErrorMessage(
|
|
error,
|
|
'生成角色动画失败',
|
|
),
|
|
}));
|
|
return;
|
|
}
|
|
setCharacterAnimationPanel((currentPanel) =>
|
|
currentPanel
|
|
? {
|
|
...currentPanel,
|
|
status: 'failed',
|
|
errorMessage: resolveImageGenerationErrorMessage(
|
|
error,
|
|
'生成角色动画失败',
|
|
),
|
|
}
|
|
: currentPanel,
|
|
);
|
|
}
|
|
}, [
|
|
addCharacterAnimationResultLayer,
|
|
characterAnimationDialog,
|
|
characterAnimationPanel,
|
|
characterAnimationSourceLayer,
|
|
getGeneratingDialogPlaceholder,
|
|
applyProjectSnapshot,
|
|
projectId,
|
|
setCharacterAnimationPanel,
|
|
updateCanvasGenerationDialogById,
|
|
onWalletBalanceMayHaveChanged,
|
|
]);
|
|
|
|
return useMemo(
|
|
() => ({
|
|
submitIconSpritesheetGeneration,
|
|
extractUiDesignAssets,
|
|
submitQuickEdit,
|
|
submitImageGeneration,
|
|
submitCharacterAnimation,
|
|
}),
|
|
[
|
|
submitCharacterAnimation,
|
|
extractUiDesignAssets,
|
|
submitIconSpritesheetGeneration,
|
|
submitImageGeneration,
|
|
submitQuickEdit,
|
|
],
|
|
);
|
|
}
|