为角色形象规范、UI素材规范、自定义规范面板新增参考图上传入口。 生成规范时携带参考图并自动追加参考图生成规范语义。 补充生成流程和上传流程回归测试。 更新画板角色形象生成入口设计文档。
1168 lines
36 KiB
TypeScript
1168 lines
36 KiB
TypeScript
import {
|
|
useCallback,
|
|
useMemo,
|
|
useState,
|
|
type Dispatch,
|
|
type MutableRefObject,
|
|
type SetStateAction,
|
|
} from 'react';
|
|
|
|
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
|
|
import {
|
|
editEditorImage,
|
|
generateEditorCharacterAnimation,
|
|
generateEditorIconSpritesheet,
|
|
generateEditorImage,
|
|
type EditorIconSpritesheetGenerationResult,
|
|
type EditorIconSpritesheetIconResult,
|
|
type EditorImageGenerationResult,
|
|
} from '../../services/image-editor/editorProjectClient';
|
|
import {
|
|
createGeneratedResultLayer,
|
|
createIconSpritesheetResultLayers,
|
|
createQuickEditResultLayer,
|
|
} from './ImageCanvasGenerationLayerModel';
|
|
import {
|
|
CHARACTER_ANIMATION_DURATION_OPTIONS,
|
|
CHARACTER_ANIMATION_MODEL,
|
|
CHARACTER_FRAME_DISPLAY_SIZE,
|
|
CHARACTER_FRAME_ORIGINAL_SIZE,
|
|
DEFAULT_ICON_DESCRIPTIONS,
|
|
DEFAULT_IMAGE_MODEL,
|
|
DEFAULT_SPEC_FORM_VALUES,
|
|
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
|
ICON_DESCRIPTION_LIMIT,
|
|
ICON_FRAME_DISPLAY_SIZE,
|
|
ICON_FRAME_ORIGINAL_SIZE,
|
|
SPEC_FRAME_DISPLAY_SIZE,
|
|
SPEC_FRAME_ORIGINAL_SIZE,
|
|
SPEC_GENERATION_SIZE,
|
|
SPEC_TYPE_LABEL,
|
|
buildCharacterGenerationInputs,
|
|
buildEditGenerationInputs,
|
|
buildIconGenerationInputs,
|
|
buildImageGenerationInputs,
|
|
buildQuickEditModelOptions,
|
|
buildQuickEditSizeOptions,
|
|
buildSpecGenerationInputs,
|
|
buildSpecPrompt,
|
|
calculateCharacterAnimationPrice,
|
|
createCanvasLayerReference,
|
|
isCanvasGenerationDialog,
|
|
resolveCharacterAnimationSourceImageSrc,
|
|
resolveImageGenerationErrorMessage,
|
|
} from './ImageCanvasGenerationModel';
|
|
import { formatImageSizeValue } from './ImageCanvasEditorModel';
|
|
import type {
|
|
CanvasGenerationDialogState,
|
|
CanvasGenerationInputs,
|
|
CanvasLayer,
|
|
CanvasTool,
|
|
CanvasViewport,
|
|
CharacterAnimationPanelState,
|
|
GenerateDialogState,
|
|
ImageContextMenuState,
|
|
QuickEditPanelState,
|
|
SidebarPanel,
|
|
SpecFormValues,
|
|
SpecGenerationType,
|
|
} from './ImageCanvasEditorTypes';
|
|
|
|
type CanvasSize = { width: number; height: number };
|
|
|
|
type CanvasGenerationDialogUpdater = (
|
|
dialog: CanvasGenerationDialogState,
|
|
) => CanvasGenerationDialogState | null;
|
|
|
|
type GenerationWorkflowOptions = {
|
|
layers: CanvasLayer[];
|
|
canvasSize: CanvasSize;
|
|
viewport: CanvasViewport;
|
|
layerCounterRef: MutableRefObject<number>;
|
|
generateDialog: GenerateDialogState | null;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
openCanvasGenerationDialog: (
|
|
dialog: Omit<CanvasGenerationDialogState, 'id'>,
|
|
) => void;
|
|
updateCanvasGenerationDialogById: (
|
|
dialogId: string,
|
|
updater: CanvasGenerationDialogUpdater,
|
|
) => void;
|
|
removeCanvasGenerationDialogById: (dialogId: string) => void;
|
|
removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void;
|
|
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>>;
|
|
setMetadataLayer: Dispatch<SetStateAction<CanvasLayer | null>>;
|
|
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
|
|
};
|
|
|
|
function getViewportWorldCenter({
|
|
canvasSize,
|
|
viewport,
|
|
}: {
|
|
canvasSize: CanvasSize;
|
|
viewport: CanvasViewport;
|
|
}) {
|
|
const safeScale = viewport.scale > 0 ? viewport.scale : 1;
|
|
return {
|
|
x: (canvasSize.width / 2 - viewport.x) / safeScale,
|
|
y: (canvasSize.height / 2 - viewport.y) / safeScale,
|
|
};
|
|
}
|
|
|
|
function setFailedCharacterGenerationIdle(dialog: GenerateDialogState) {
|
|
return {
|
|
...dialog,
|
|
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
};
|
|
}
|
|
|
|
function setFailedIconGenerationIdle(dialog: GenerateDialogState) {
|
|
return {
|
|
...dialog,
|
|
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
};
|
|
}
|
|
|
|
export function useImageCanvasGenerationWorkflow({
|
|
layers,
|
|
canvasSize,
|
|
viewport,
|
|
layerCounterRef,
|
|
generateDialog,
|
|
setGenerateDialog,
|
|
openCanvasGenerationDialog,
|
|
updateCanvasGenerationDialogById,
|
|
removeCanvasGenerationDialogById,
|
|
removeCanvasGenerationDialogsByLayerId,
|
|
getGeneratingDialogPlaceholder,
|
|
appendCanvasLayersWithResources,
|
|
selectSingleLayer,
|
|
fitLayers,
|
|
setActiveTool,
|
|
setActiveSidebarPanel,
|
|
setMetadataLayer,
|
|
setImageContextMenu,
|
|
}: GenerationWorkflowOptions) {
|
|
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
|
|
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] =
|
|
useState(false);
|
|
const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] =
|
|
useState(false);
|
|
const [
|
|
isPickingCharacterSpecFromCanvas,
|
|
setIsPickingCharacterSpecFromCanvas,
|
|
] = useState(false);
|
|
const [
|
|
isPickingCharacterReferenceFromCanvas,
|
|
setIsPickingCharacterReferenceFromCanvas,
|
|
] = useState(false);
|
|
const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(false);
|
|
const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
|
|
useState(false);
|
|
const [quickEditPanel, setQuickEditPanel] =
|
|
useState<QuickEditPanelState | null>(null);
|
|
const [characterAnimationPanel, setCharacterAnimationPanel] =
|
|
useState<CharacterAnimationPanelState | null>(null);
|
|
const [lastImageModel, setLastImageModel] = useState(DEFAULT_IMAGE_MODEL);
|
|
|
|
const quickEditSourceLayer = quickEditPanel
|
|
? (layers.find((layer) => layer.id === quickEditPanel.sourceLayerId) ??
|
|
null)
|
|
: null;
|
|
const characterAnimationSourceLayer = characterAnimationPanel
|
|
? (layers.find(
|
|
(layer) => layer.id === characterAnimationPanel.sourceLayerId,
|
|
) ?? null)
|
|
: null;
|
|
const quickEditSizeOptions = quickEditPanel
|
|
? buildQuickEditSizeOptions(quickEditPanel.size)
|
|
: [];
|
|
const quickEditModelOptions = quickEditPanel
|
|
? buildQuickEditModelOptions(quickEditPanel.model)
|
|
: [];
|
|
const characterAnimationPrice = characterAnimationPanel
|
|
? calculateCharacterAnimationPrice(
|
|
characterAnimationPanel.resolution,
|
|
characterAnimationPanel.durationSeconds,
|
|
)
|
|
: 0;
|
|
const iconDescriptionValues =
|
|
generateDialog?.mode === 'icon'
|
|
? (generateDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS)
|
|
: DEFAULT_ICON_DESCRIPTIONS;
|
|
|
|
const openGenerateDialog = useCallback(() => {
|
|
const placeholderWidth = 420;
|
|
const placeholderHeight = 420;
|
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
|
openCanvasGenerationDialog({
|
|
mode: 'generate',
|
|
prompt: '',
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
placeholder: {
|
|
x: worldCenter.x - placeholderWidth / 2,
|
|
y: worldCenter.y - placeholderHeight / 2,
|
|
width: placeholderWidth,
|
|
height: placeholderHeight,
|
|
originalWidth: 2048,
|
|
originalHeight: 2048,
|
|
},
|
|
});
|
|
setActiveTool('generate');
|
|
selectSingleLayer(null);
|
|
setQuickEditPanel(null);
|
|
}, [
|
|
canvasSize,
|
|
openCanvasGenerationDialog,
|
|
selectSingleLayer,
|
|
setActiveTool,
|
|
viewport,
|
|
]);
|
|
|
|
const openSpecDialog = useCallback(
|
|
(specType: SpecGenerationType) => {
|
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
|
openCanvasGenerationDialog({
|
|
mode: 'spec',
|
|
prompt: '',
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
specType,
|
|
specValues: { ...DEFAULT_SPEC_FORM_VALUES[specType] },
|
|
placeholder: {
|
|
x: worldCenter.x - SPEC_FRAME_DISPLAY_SIZE.width / 2,
|
|
y: worldCenter.y - SPEC_FRAME_DISPLAY_SIZE.height / 2,
|
|
width: SPEC_FRAME_DISPLAY_SIZE.width,
|
|
height: SPEC_FRAME_DISPLAY_SIZE.height,
|
|
originalWidth: SPEC_FRAME_ORIGINAL_SIZE.width,
|
|
originalHeight: SPEC_FRAME_ORIGINAL_SIZE.height,
|
|
},
|
|
});
|
|
setIsSpecMenuOpen(false);
|
|
setActiveTool('generate');
|
|
selectSingleLayer(null);
|
|
setQuickEditPanel(null);
|
|
},
|
|
[
|
|
canvasSize,
|
|
openCanvasGenerationDialog,
|
|
selectSingleLayer,
|
|
setActiveTool,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
const openCharacterAnimationPanel = useCallback(
|
|
(layer: CanvasLayer) => {
|
|
if (layer.assetKind !== 'character') {
|
|
return;
|
|
}
|
|
setImageContextMenu(null);
|
|
setQuickEditPanel(null);
|
|
setCharacterAnimationPanel({
|
|
sourceLayerId: layer.id,
|
|
promptText: '',
|
|
resolution: '480p',
|
|
ratio: 'same',
|
|
frameCount: 32,
|
|
durationSeconds: 4,
|
|
status: 'idle',
|
|
});
|
|
selectSingleLayer(layer.id);
|
|
},
|
|
[selectSingleLayer, setImageContextMenu],
|
|
);
|
|
|
|
const openCharacterGenerationDialog = useCallback(() => {
|
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
|
setIsSpecMenuOpen(false);
|
|
setIsCharacterReferenceMenuOpen(false);
|
|
setIsPickingCharacterSpecFromCanvas(false);
|
|
setIsPickingCharacterReferenceFromCanvas(false);
|
|
const dimensionOptions =
|
|
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
|
lastImageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
|
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
|
|
openCanvasGenerationDialog({
|
|
mode: 'character',
|
|
prompt: '',
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
characterSpecReference: null,
|
|
characterReferences: [],
|
|
imageModel: lastImageModel,
|
|
aspectRatio: dimensionOptions.aspectRatios[0],
|
|
imageSize:
|
|
dimensionOptions.imageSizes.find((size) => size === '1K') ??
|
|
dimensionOptions.imageSizes[0],
|
|
placeholder: {
|
|
x: worldCenter.x - CHARACTER_FRAME_DISPLAY_SIZE.width / 2,
|
|
y: worldCenter.y - CHARACTER_FRAME_DISPLAY_SIZE.height / 2,
|
|
width: CHARACTER_FRAME_DISPLAY_SIZE.width,
|
|
height: CHARACTER_FRAME_DISPLAY_SIZE.height,
|
|
originalWidth: CHARACTER_FRAME_ORIGINAL_SIZE.width,
|
|
originalHeight: CHARACTER_FRAME_ORIGINAL_SIZE.height,
|
|
},
|
|
});
|
|
setActiveTool('character');
|
|
selectSingleLayer(null);
|
|
setQuickEditPanel(null);
|
|
}, [
|
|
canvasSize,
|
|
lastImageModel,
|
|
openCanvasGenerationDialog,
|
|
selectSingleLayer,
|
|
setActiveTool,
|
|
viewport,
|
|
]);
|
|
|
|
const openIconGenerationDialog = useCallback(() => {
|
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
|
setIsSpecMenuOpen(false);
|
|
setIsCharacterReferenceMenuOpen(false);
|
|
setIsPickingCharacterSpecFromCanvas(false);
|
|
setIsPickingCharacterReferenceFromCanvas(false);
|
|
setIsPickingIconSpecFromCanvas(false);
|
|
const dimensionOptions =
|
|
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
|
lastImageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
|
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
|
|
openCanvasGenerationDialog({
|
|
mode: 'icon',
|
|
prompt: '',
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
iconSpecReference: null,
|
|
iconDescriptions: [...DEFAULT_ICON_DESCRIPTIONS],
|
|
imageModel: lastImageModel,
|
|
aspectRatio: dimensionOptions.aspectRatios[0],
|
|
imageSize:
|
|
dimensionOptions.imageSizes.find((size) => size === '1K') ??
|
|
dimensionOptions.imageSizes[0],
|
|
placeholder: {
|
|
x: worldCenter.x - ICON_FRAME_DISPLAY_SIZE.width / 2,
|
|
y: worldCenter.y - ICON_FRAME_DISPLAY_SIZE.height / 2,
|
|
width: ICON_FRAME_DISPLAY_SIZE.width,
|
|
height: ICON_FRAME_DISPLAY_SIZE.height,
|
|
originalWidth: ICON_FRAME_ORIGINAL_SIZE.width,
|
|
originalHeight: ICON_FRAME_ORIGINAL_SIZE.height,
|
|
},
|
|
});
|
|
setActiveTool('icon');
|
|
selectSingleLayer(null);
|
|
setQuickEditPanel(null);
|
|
setCharacterAnimationPanel(null);
|
|
}, [
|
|
canvasSize,
|
|
lastImageModel,
|
|
openCanvasGenerationDialog,
|
|
selectSingleLayer,
|
|
setActiveTool,
|
|
viewport,
|
|
]);
|
|
|
|
const openEditDialog = useCallback(
|
|
(sourceLayer: CanvasLayer) => {
|
|
setMetadataLayer(null);
|
|
setImageContextMenu(null);
|
|
setQuickEditPanel(null);
|
|
setGenerateDialog({
|
|
mode: 'edit',
|
|
prompt: sourceLayer.prompt
|
|
? `${sourceLayer.prompt},在保持主体结构的基础上优化画面细节`
|
|
: '',
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
sourceLayerId: sourceLayer.id,
|
|
});
|
|
setActiveTool('generate');
|
|
},
|
|
[setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer],
|
|
);
|
|
|
|
const openQuickEditPanel = useCallback(
|
|
(sourceLayer: CanvasLayer) => {
|
|
setImageContextMenu(null);
|
|
setMetadataLayer(null);
|
|
setGenerateDialog(null);
|
|
setCharacterAnimationPanel(null);
|
|
setQuickEditPanel({
|
|
sourceLayerId: sourceLayer.id,
|
|
prompt: '',
|
|
size: formatImageSizeValue(
|
|
sourceLayer.originalWidth,
|
|
sourceLayer.originalHeight,
|
|
),
|
|
model: sourceLayer.model?.trim() || DEFAULT_IMAGE_MODEL,
|
|
status: 'idle',
|
|
});
|
|
selectSingleLayer(sourceLayer.id);
|
|
setActiveTool('generate');
|
|
},
|
|
[
|
|
selectSingleLayer,
|
|
setActiveTool,
|
|
setGenerateDialog,
|
|
setImageContextMenu,
|
|
setMetadataLayer,
|
|
],
|
|
);
|
|
|
|
const addGeneratedResultLayer = useCallback(
|
|
(
|
|
generated: EditorImageGenerationResult,
|
|
options: {
|
|
sourceLayer?: CanvasLayer;
|
|
frame?: GenerateDialogState['placeholder'];
|
|
assetKind?: CanvasLayer['assetKind'];
|
|
title?: string;
|
|
dialogId?: string;
|
|
generationInputs?: CanvasGenerationInputs;
|
|
} = {},
|
|
) => {
|
|
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,
|
|
});
|
|
|
|
appendCanvasLayersWithResources([nextLayer]);
|
|
selectSingleLayer(nextLayer.id);
|
|
setActiveSidebarPanel('layers');
|
|
if (options.sourceLayer) {
|
|
setGenerateDialog(null);
|
|
setActiveTool('select');
|
|
} else if (options.dialogId) {
|
|
updateCanvasGenerationDialogById(options.dialogId, (currentDialog) =>
|
|
currentDialog.mode === 'character' || currentDialog.mode === 'icon'
|
|
? null
|
|
: {
|
|
...currentDialog,
|
|
status: 'idle',
|
|
composerOpen: true,
|
|
generatedLayerId: nextLayer.id,
|
|
placeholder: undefined,
|
|
errorMessage: undefined,
|
|
},
|
|
);
|
|
}
|
|
if (options.sourceLayer) {
|
|
fitLayers([options.sourceLayer, nextLayer]);
|
|
}
|
|
},
|
|
[
|
|
appendCanvasLayersWithResources,
|
|
canvasSize,
|
|
fitLayers,
|
|
layerCounterRef,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
setGenerateDialog,
|
|
updateCanvasGenerationDialogById,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
const addQuickEditResultLayer = useCallback(
|
|
(
|
|
generated: EditorImageGenerationResult,
|
|
sourceLayer: CanvasLayer,
|
|
generationInputs: CanvasGenerationInputs,
|
|
) => {
|
|
layerCounterRef.current += 1;
|
|
const generatedIndex = layerCounterRef.current;
|
|
const nextLayer = createQuickEditResultLayer({
|
|
generated,
|
|
generatedIndex,
|
|
sourceLayer,
|
|
generationInputs,
|
|
});
|
|
|
|
appendCanvasLayersWithResources([nextLayer]);
|
|
selectSingleLayer(nextLayer.id);
|
|
setActiveSidebarPanel('layers');
|
|
setQuickEditPanel(null);
|
|
setActiveTool('select');
|
|
fitLayers([sourceLayer, nextLayer]);
|
|
},
|
|
[
|
|
appendCanvasLayersWithResources,
|
|
fitLayers,
|
|
layerCounterRef,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
],
|
|
);
|
|
|
|
const addIconSpritesheetResultLayers = useCallback(
|
|
(
|
|
generated: EditorIconSpritesheetGenerationResult,
|
|
iconResults: EditorIconSpritesheetIconResult[],
|
|
generationInputs: CanvasGenerationInputs,
|
|
frame?: GenerateDialogState['placeholder'],
|
|
dialogId?: string,
|
|
) => {
|
|
const startIndex = layerCounterRef.current + 1;
|
|
const nextLayers = createIconSpritesheetResultLayers({
|
|
generated,
|
|
iconResults,
|
|
startIndex,
|
|
canvasSize,
|
|
viewport,
|
|
generationInputs,
|
|
frame,
|
|
});
|
|
|
|
if (!nextLayers.length) {
|
|
return;
|
|
}
|
|
layerCounterRef.current += nextLayers.length;
|
|
appendCanvasLayersWithResources(nextLayers);
|
|
selectSingleLayer(nextLayers[0]?.id ?? null);
|
|
setActiveSidebarPanel('layers');
|
|
if (dialogId) {
|
|
removeCanvasGenerationDialogById(dialogId);
|
|
}
|
|
setActiveTool('select');
|
|
},
|
|
[
|
|
appendCanvasLayersWithResources,
|
|
canvasSize,
|
|
layerCounterRef,
|
|
removeCanvasGenerationDialogById,
|
|
selectSingleLayer,
|
|
setActiveSidebarPanel,
|
|
setActiveTool,
|
|
viewport,
|
|
],
|
|
);
|
|
|
|
const pickCharacterSpecFromLayer = useCallback(
|
|
(layer: CanvasLayer) => {
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'character'
|
|
? {
|
|
...setFailedCharacterGenerationIdle(currentDialog),
|
|
characterSpecReference: createCanvasLayerReference(layer),
|
|
composerOpen: true,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
setIsPickingCharacterSpecFromCanvas(false);
|
|
setIsCharacterSpecMenuOpen(false);
|
|
setImageContextMenu(null);
|
|
},
|
|
[setGenerateDialog, setImageContextMenu],
|
|
);
|
|
|
|
const pickCharacterReferenceFromLayer = useCallback(
|
|
(layer: CanvasLayer) => {
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'character'
|
|
? {
|
|
...setFailedCharacterGenerationIdle(currentDialog),
|
|
characterReferences: [
|
|
...(currentDialog.characterReferences ?? []),
|
|
createCanvasLayerReference(layer),
|
|
],
|
|
composerOpen: true,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
setIsPickingCharacterReferenceFromCanvas(false);
|
|
setImageContextMenu(null);
|
|
},
|
|
[setGenerateDialog, setImageContextMenu],
|
|
);
|
|
|
|
const pickIconSpecFromLayer = useCallback(
|
|
(layer: CanvasLayer) => {
|
|
if (layer.assetKind !== 'icon-spec') {
|
|
return;
|
|
}
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'icon'
|
|
? {
|
|
...setFailedIconGenerationIdle(currentDialog),
|
|
iconSpecReference: createCanvasLayerReference(layer),
|
|
composerOpen: true,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
setIsPickingIconSpecFromCanvas(false);
|
|
setIsIconSpecMenuOpen(false);
|
|
setImageContextMenu(null);
|
|
},
|
|
[setGenerateDialog, setImageContextMenu],
|
|
);
|
|
|
|
const updateIconDescription = useCallback(
|
|
(index: number, value: string) => {
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'icon'
|
|
? {
|
|
...setFailedIconGenerationIdle(currentDialog),
|
|
iconDescriptions: (
|
|
currentDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS
|
|
).map((description, descriptionIndex) =>
|
|
descriptionIndex === index ? value : description,
|
|
),
|
|
}
|
|
: currentDialog,
|
|
);
|
|
},
|
|
[setGenerateDialog],
|
|
);
|
|
|
|
const addIconDescription = useCallback(() => {
|
|
setGenerateDialog((currentDialog) => {
|
|
if (currentDialog?.mode !== 'icon') {
|
|
return currentDialog;
|
|
}
|
|
const descriptions =
|
|
currentDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS;
|
|
if (descriptions.length >= ICON_DESCRIPTION_LIMIT) {
|
|
return currentDialog;
|
|
}
|
|
return {
|
|
...setFailedIconGenerationIdle(currentDialog),
|
|
iconDescriptions: [...descriptions, ''],
|
|
};
|
|
});
|
|
}, [setGenerateDialog]);
|
|
|
|
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 iconDescriptions = (
|
|
dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS
|
|
)
|
|
.map((description) => description.trim())
|
|
.filter(Boolean);
|
|
if (!dialog.iconSpecReference) {
|
|
if (canvasDialog) {
|
|
setSubmittingIconDialog({
|
|
...canvasDialog,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage: '请选择图标素材规范',
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
if (!iconDescriptions.length) {
|
|
if (canvasDialog) {
|
|
setSubmittingIconDialog({
|
|
...canvasDialog,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage: '请填写素材描述',
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!canvasDialog) {
|
|
return;
|
|
}
|
|
|
|
setSubmittingIconDialog({
|
|
...canvasDialog,
|
|
iconDescriptions,
|
|
status: 'generating',
|
|
composerOpen: false,
|
|
errorMessage: undefined,
|
|
});
|
|
|
|
try {
|
|
const generated = await generateEditorIconSpritesheet({
|
|
referenceImageSrc: dialog.iconSpecReference.src,
|
|
iconDescriptions,
|
|
model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL,
|
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
|
imageSize: dialog.imageSize ?? '1K',
|
|
});
|
|
setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL);
|
|
addIconSpritesheetResultLayers(
|
|
generated,
|
|
generated.iconImageSrcs,
|
|
buildIconGenerationInputs(iconDescriptions, dialog.iconSpecReference),
|
|
getGeneratingDialogPlaceholder(dialog),
|
|
canvasDialog.id,
|
|
);
|
|
} catch (error) {
|
|
setSubmittingIconDialog({
|
|
...canvasDialog,
|
|
iconDescriptions,
|
|
status: 'failed',
|
|
composerOpen: true,
|
|
errorMessage: resolveImageGenerationErrorMessage(error),
|
|
});
|
|
}
|
|
},
|
|
[
|
|
addIconSpritesheetResultLayers,
|
|
getGeneratingDialogPlaceholder,
|
|
updateCanvasGenerationDialogById,
|
|
],
|
|
);
|
|
|
|
const submitQuickEdit = useCallback(async () => {
|
|
if (!quickEditPanel || !quickEditSourceLayer) {
|
|
return;
|
|
}
|
|
|
|
const normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片';
|
|
setQuickEditPanel({
|
|
...quickEditPanel,
|
|
prompt: normalizedPrompt,
|
|
status: 'generating',
|
|
errorMessage: undefined,
|
|
});
|
|
|
|
try {
|
|
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
|
quickEditSourceLayer.src,
|
|
);
|
|
const generated = await generateEditorImage({
|
|
prompt: normalizedPrompt,
|
|
size: quickEditPanel.size,
|
|
kind: 'quick-edit',
|
|
model: quickEditPanel.model,
|
|
referenceImageSrcs: [referenceImageSrc],
|
|
});
|
|
addQuickEditResultLayer(
|
|
generated,
|
|
quickEditSourceLayer,
|
|
buildEditGenerationInputs(
|
|
'快速编辑提示词',
|
|
normalizedPrompt,
|
|
quickEditSourceLayer,
|
|
),
|
|
);
|
|
} catch (error) {
|
|
setQuickEditPanel({
|
|
...quickEditPanel,
|
|
prompt: normalizedPrompt,
|
|
status: 'failed',
|
|
errorMessage: resolveImageGenerationErrorMessage(error),
|
|
});
|
|
}
|
|
}, [addQuickEditResultLayer, quickEditPanel, quickEditSourceLayer]);
|
|
|
|
const submitImageGeneration = useCallback(
|
|
async (dialog: GenerateDialogState) => {
|
|
const normalizedPrompt =
|
|
dialog.prompt.trim() ||
|
|
(dialog.mode === 'edit' ? '修改当前图片' : '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 {
|
|
if (dialog.mode === 'edit') {
|
|
const sourceLayer = layers.find(
|
|
(layer) => layer.id === dialog.sourceLayerId,
|
|
);
|
|
if (!sourceLayer) {
|
|
throw new Error('未找到要修改的图片');
|
|
}
|
|
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
|
sourceLayer.src,
|
|
);
|
|
const generated = await editEditorImage({
|
|
prompt: normalizedPrompt,
|
|
sourceImageSrc: referenceImageSrc,
|
|
});
|
|
addGeneratedResultLayer(generated, {
|
|
sourceLayer,
|
|
generationInputs: buildEditGenerationInputs(
|
|
'修改要求',
|
|
normalizedPrompt,
|
|
sourceLayer,
|
|
),
|
|
});
|
|
} else if (dialog.mode === 'spec') {
|
|
const specType = dialog.specType ?? 'custom';
|
|
const specValues =
|
|
dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType];
|
|
const specPrompt = buildSpecPrompt(
|
|
specType,
|
|
specValues,
|
|
Boolean(dialog.specReference?.src),
|
|
);
|
|
const generated = await generateEditorImage({
|
|
prompt: specPrompt,
|
|
size: SPEC_GENERATION_SIZE,
|
|
model: DEFAULT_IMAGE_MODEL,
|
|
kind: 'spec',
|
|
...(dialog.specReference?.src
|
|
? { referenceImageSrcs: [dialog.specReference.src] }
|
|
: {}),
|
|
});
|
|
addGeneratedResultLayer(generated, {
|
|
frame: getGeneratingDialogPlaceholder(dialog),
|
|
assetKind: specType === 'icon' ? 'icon-spec' : 'spec',
|
|
title: `${SPEC_TYPE_LABEL[specType]} ${layerCounterRef.current + 1}`,
|
|
dialogId: canvasDialog?.id,
|
|
generationInputs: buildSpecGenerationInputs(
|
|
specType,
|
|
specValues,
|
|
dialog.specReference,
|
|
),
|
|
});
|
|
} else if (dialog.mode === 'character') {
|
|
const referenceImageSrcs = [
|
|
dialog.characterSpecReference?.src,
|
|
...(dialog.characterReferences ?? []).map(
|
|
(reference) => reference.src,
|
|
),
|
|
].filter((src): src is string => Boolean(src));
|
|
const generated = await generateEditorImage({
|
|
prompt: normalizedPrompt,
|
|
kind: 'character',
|
|
model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL,
|
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
|
imageSize: dialog.imageSize ?? '1K',
|
|
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
|
});
|
|
setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL);
|
|
addGeneratedResultLayer(generated, {
|
|
frame: getGeneratingDialogPlaceholder(dialog),
|
|
assetKind: 'character',
|
|
title: `角色形象 ${layerCounterRef.current + 1}`,
|
|
dialogId: canvasDialog?.id,
|
|
generationInputs: buildCharacterGenerationInputs(
|
|
normalizedPrompt,
|
|
dialog.characterSpecReference,
|
|
dialog.characterReferences,
|
|
),
|
|
});
|
|
} else {
|
|
const generated = await generateEditorImage({
|
|
prompt: normalizedPrompt,
|
|
});
|
|
addGeneratedResultLayer(generated, {
|
|
frame: getGeneratingDialogPlaceholder(dialog),
|
|
dialogId: canvasDialog?.id,
|
|
generationInputs: buildImageGenerationInputs(normalizedPrompt),
|
|
});
|
|
}
|
|
} 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,
|
|
getGeneratingDialogPlaceholder,
|
|
layerCounterRef,
|
|
layers,
|
|
setGenerateDialog,
|
|
updateCanvasGenerationDialogById,
|
|
],
|
|
);
|
|
|
|
const updateSpecFormValue = useCallback(
|
|
(key: keyof SpecFormValues, value: string) => {
|
|
setGenerateDialog((currentDialog) => {
|
|
if (currentDialog?.mode !== 'spec') {
|
|
return currentDialog;
|
|
}
|
|
const specType = currentDialog.specType ?? 'custom';
|
|
return {
|
|
...currentDialog,
|
|
specValues: {
|
|
...DEFAULT_SPEC_FORM_VALUES[specType],
|
|
...currentDialog.specValues,
|
|
[key]: value,
|
|
},
|
|
status:
|
|
currentDialog.status === 'failed' ? 'idle' : currentDialog.status,
|
|
errorMessage:
|
|
currentDialog.status === 'failed'
|
|
? undefined
|
|
: currentDialog.errorMessage,
|
|
};
|
|
});
|
|
},
|
|
[setGenerateDialog],
|
|
);
|
|
|
|
const updateCharacterAnimationDuration = useCallback(
|
|
(frameCountValue: string) => {
|
|
const option = CHARACTER_ANIMATION_DURATION_OPTIONS.find(
|
|
(item) => String(item.frameCount) === frameCountValue,
|
|
);
|
|
if (!option) {
|
|
return;
|
|
}
|
|
setCharacterAnimationPanel((currentPanel) =>
|
|
currentPanel
|
|
? {
|
|
...currentPanel,
|
|
frameCount: option.frameCount,
|
|
durationSeconds: option.durationSeconds,
|
|
status:
|
|
currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
|
|
errorMessage:
|
|
currentPanel.status === 'failed'
|
|
? undefined
|
|
: currentPanel.errorMessage,
|
|
}
|
|
: currentPanel,
|
|
);
|
|
},
|
|
[],
|
|
);
|
|
|
|
const submitCharacterAnimation = useCallback(async () => {
|
|
if (!characterAnimationPanel || !characterAnimationSourceLayer) {
|
|
return;
|
|
}
|
|
const promptText = characterAnimationPanel.promptText.trim();
|
|
const nextPanel = {
|
|
...characterAnimationPanel,
|
|
promptText,
|
|
status: 'generating' as const,
|
|
errorMessage: undefined,
|
|
result: undefined,
|
|
};
|
|
setCharacterAnimationPanel(nextPanel);
|
|
|
|
try {
|
|
const result = await generateEditorCharacterAnimation({
|
|
sourceLayerId: characterAnimationSourceLayer.id,
|
|
sourceImageSrc: resolveCharacterAnimationSourceImageSrc(
|
|
characterAnimationSourceLayer,
|
|
),
|
|
sourceWidth: characterAnimationSourceLayer.originalWidth,
|
|
sourceHeight: characterAnimationSourceLayer.originalHeight,
|
|
promptText,
|
|
resolution: nextPanel.resolution,
|
|
ratio: nextPanel.ratio,
|
|
frameCount: nextPanel.frameCount,
|
|
durationSeconds: nextPanel.durationSeconds,
|
|
priceMudPoints: calculateCharacterAnimationPrice(
|
|
nextPanel.resolution,
|
|
nextPanel.durationSeconds,
|
|
),
|
|
model: CHARACTER_ANIMATION_MODEL,
|
|
});
|
|
setCharacterAnimationPanel((currentPanel) =>
|
|
currentPanel
|
|
? {
|
|
...currentPanel,
|
|
status: 'completed',
|
|
result,
|
|
}
|
|
: currentPanel,
|
|
);
|
|
} catch (error) {
|
|
setCharacterAnimationPanel((currentPanel) =>
|
|
currentPanel
|
|
? {
|
|
...currentPanel,
|
|
status: 'failed',
|
|
errorMessage:
|
|
error instanceof Error && error.message.trim()
|
|
? error.message
|
|
: '生成角色动画失败',
|
|
}
|
|
: currentPanel,
|
|
);
|
|
}
|
|
}, [characterAnimationPanel, characterAnimationSourceLayer]);
|
|
|
|
const hideGeneratedLayerPanelAfterBlur = useCallback(() => {
|
|
setGenerateDialog((currentDialog) =>
|
|
(currentDialog?.mode === 'generate' ||
|
|
currentDialog?.mode === 'spec' ||
|
|
currentDialog?.mode === 'character' ||
|
|
currentDialog?.mode === 'icon') &&
|
|
currentDialog.status !== 'generating'
|
|
? {
|
|
...currentDialog,
|
|
composerOpen: false,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
}, [setGenerateDialog]);
|
|
|
|
const closeGenerateComposer = useCallback(() => {
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'generate'
|
|
? {
|
|
...currentDialog,
|
|
composerOpen: false,
|
|
}
|
|
: currentDialog,
|
|
);
|
|
setActiveTool('select');
|
|
}, [setActiveTool, setGenerateDialog]);
|
|
|
|
const clearDeletedLayerGenerationState = useCallback(
|
|
(targetLayerId: string) => {
|
|
setQuickEditPanel((currentPanel) =>
|
|
currentPanel?.sourceLayerId === targetLayerId ? null : currentPanel,
|
|
);
|
|
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,
|
|
quickEditSizeOptions,
|
|
quickEditModelOptions,
|
|
characterAnimationPanel,
|
|
setCharacterAnimationPanel,
|
|
characterAnimationSourceLayer,
|
|
characterAnimationPrice,
|
|
iconDescriptionValues,
|
|
isSpecMenuOpen,
|
|
setIsSpecMenuOpen,
|
|
isCharacterSpecMenuOpen,
|
|
setIsCharacterSpecMenuOpen,
|
|
isCharacterReferenceMenuOpen,
|
|
setIsCharacterReferenceMenuOpen,
|
|
isPickingCharacterSpecFromCanvas,
|
|
setIsPickingCharacterSpecFromCanvas,
|
|
isPickingCharacterReferenceFromCanvas,
|
|
setIsPickingCharacterReferenceFromCanvas,
|
|
isIconSpecMenuOpen,
|
|
setIsIconSpecMenuOpen,
|
|
isPickingIconSpecFromCanvas,
|
|
setIsPickingIconSpecFromCanvas,
|
|
openGenerateDialog,
|
|
openSpecDialog,
|
|
openCharacterAnimationPanel,
|
|
openCharacterGenerationDialog,
|
|
openIconGenerationDialog,
|
|
openEditDialog,
|
|
openQuickEditPanel,
|
|
pickCharacterSpecFromLayer,
|
|
pickCharacterReferenceFromLayer,
|
|
pickIconSpecFromLayer,
|
|
submitIconSpritesheetGeneration,
|
|
submitQuickEdit,
|
|
submitImageGeneration,
|
|
updateSpecFormValue,
|
|
updateIconDescription,
|
|
addIconDescription,
|
|
updateCharacterAnimationDuration,
|
|
rememberImageModel: setLastImageModel,
|
|
submitCharacterAnimation,
|
|
hideGeneratedLayerPanelAfterBlur,
|
|
closeGenerateComposer,
|
|
clearDeletedLayerGenerationState,
|
|
}),
|
|
[
|
|
addIconDescription,
|
|
characterAnimationPanel,
|
|
characterAnimationPrice,
|
|
characterAnimationSourceLayer,
|
|
clearDeletedLayerGenerationState,
|
|
closeGenerateComposer,
|
|
hideGeneratedLayerPanelAfterBlur,
|
|
iconDescriptionValues,
|
|
isCharacterReferenceMenuOpen,
|
|
isCharacterSpecMenuOpen,
|
|
isIconSpecMenuOpen,
|
|
isPickingCharacterReferenceFromCanvas,
|
|
isPickingCharacterSpecFromCanvas,
|
|
isPickingIconSpecFromCanvas,
|
|
isSpecMenuOpen,
|
|
openCharacterAnimationPanel,
|
|
openCharacterGenerationDialog,
|
|
openEditDialog,
|
|
openGenerateDialog,
|
|
openIconGenerationDialog,
|
|
openQuickEditPanel,
|
|
openSpecDialog,
|
|
pickCharacterReferenceFromLayer,
|
|
pickCharacterSpecFromLayer,
|
|
pickIconSpecFromLayer,
|
|
quickEditModelOptions,
|
|
quickEditPanel,
|
|
quickEditSizeOptions,
|
|
quickEditSourceLayer,
|
|
submitCharacterAnimation,
|
|
submitIconSpritesheetGeneration,
|
|
submitImageGeneration,
|
|
submitQuickEdit,
|
|
updateCharacterAnimationDuration,
|
|
updateIconDescription,
|
|
updateSpecFormValue,
|
|
],
|
|
);
|
|
}
|