Files
Genarrative/src/components/image-editor/ImageCanvasGenerationDialogModel.ts
T
agent 3508957eda 补齐 AGC V3 C9 参考图选择弹窗与提交快照
- 新增 ImageCanvasProjectAssetPickerDialog:独立弹窗,网格卡片、多选、筛选+搜索、清空/确认/取消,筛选状态自持

- 新增 projectAssetReferencePickerModel:素材分类与筛选判据、素材 id 快照引用构造、弹窗引用前缀判定

- 生成 composer 参考图来源菜单新增「从项目素材中选择」,由宿主决定是否出现,不改变原有从画布选择与上传两条路径

- 新增 replaceProjectAssetPickerReferences:确认时整组替换素材库来源的参考图,保留画布点选与上传的参考图

- 素材 id 经 sourceAssetId 走既有 createGenerationInputReference 落进 generationInputs.references,不新增后端接口

- 新增 13 条测试覆盖弹窗交互、筛选叠加、快照落库与整组替换语义
2026-09-10 15:13:40 +08:00

2412 lines
70 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
SOUND_EFFECT_DURATION_MAX_SECONDS,
SOUND_EFFECT_DURATION_MIN_SECONDS,
} from '../../../packages/shared/src/contracts/editorAudio';
import {
DEFAULT_EDITOR_SCENE_STYLE_PRESET,
resolveEditorSceneStylePresetByLabel,
} from '../../../packages/shared/src/contracts/editorScene';
import { formatImageSizeValue } from './ImageCanvasEditorModel';
import type {
CanvasGenerationDialogState,
CanvasGenerationInputReference,
CanvasLayer,
CanvasViewport,
CharacterAnimationPanelState,
CharacterReferenceImage,
GenerateDialogState,
PublicationMaterialsWorkflowId,
QuickEditPanelState,
SpecFormValues,
SpecGenerationType,
} from './ImageCanvasEditorTypes';
import {
appendLimitedImageReferences,
appendLimitedQuickEditReferences,
AUDIO_FRAME_DISPLAY_SIZE,
AUDIO_FRAME_ORIGINAL_SIZE,
CHARACTER_ANIMATION_DURATION_OPTIONS,
CHARACTER_ANIMATION_FRAME_DISPLAY_SIZE,
CHARACTER_ANIMATION_FRAME_ORIGINAL_SIZE,
createCanvasLayerReference,
decodeCanvasGenerationInputs,
DEFAULT_IMAGE_MODEL,
DEFAULT_PUBLICATION_GAME_INFO,
DEFAULT_SOUND_EFFECT_DURATION_SECONDS,
DEFAULT_SOUND_EFFECT_MODEL,
DEFAULT_SPEC_FORM_VALUES,
DEFAULT_VIDEO_ASPECT_RATIO,
DEFAULT_VIDEO_DURATION_SECONDS,
DEFAULT_VIDEO_MODEL,
DEFAULT_VIDEO_SOUND,
DEFAULT_VIDEO_WEB_SEARCH_ENABLED,
EDITOR_IMAGE_DIMENSION_OPTIONS,
EDITOR_IMAGE_MODEL_OPTIONS,
formatGenerationInputValue,
IMAGE_MODEL_GPT_IMAGE_2,
inferEditorImageAspectRatio,
inferEditorImageSizeLabel,
isNormalizedCanvasGenerationInputs,
normalizeEditorImageModel,
PUBLICATION_FRAME_ORIGINAL_SIZE,
resizeGenerationPlaceholderToImageSelection,
resizeGenerationPlaceholderToVideoSelection,
resolveDialogExtraImageReferenceLimit,
resolveEditorImageGenerationPixelSize,
resolveEditorVideoGenerationPixelSize,
SPEC_FRAME_ORIGINAL_SIZE,
} from './ImageCanvasGenerationModel';
import { getPublicationMaterialsWorkflow } from './ImageCanvasPublicationMaterialsModel';
import { isProjectAssetPickerReference } from './projectAssetReferencePickerModel';
type CanvasSize = { width: number; height: number };
type SourceGenerationDialogDraftContext = {
sourceLayer: CanvasLayer;
canvasSize: CanvasSize;
viewport: CanvasViewport;
sourceDialog?: CanvasGenerationDialogState | null;
mode: 'quick-edit' | 'redraw';
sourceAnimationLayer?: CanvasLayer | null;
availableLayers?: CanvasLayer[];
};
const VIDEO_REFERENCE_LIMITS = {
image: 9,
video: 3,
audio: 3,
} as const;
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 resetFailedGenerationDialog(dialog: GenerateDialogState) {
return {
...dialog,
status: dialog.status === 'failed' ? 'idle' : dialog.status,
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
};
}
export function resolveSpecGenerationReferences(
dialog: GenerateDialogState | null | undefined,
) {
if (!dialog || dialog.mode !== 'spec') {
return [];
}
if (dialog.specType === 'icon') {
return dialog.specReference ? [dialog.specReference] : [];
}
if (dialog.generationReferences?.length) {
return dialog.generationReferences;
}
return dialog.specReference ? [dialog.specReference] : [];
}
export function setSpecGenerationReferences(
dialog: GenerateDialogState,
references: NonNullable<GenerateDialogState['generationReferences']>,
) {
if (dialog.mode !== 'spec') {
return dialog;
}
return dialog.specType === 'icon'
? {
...dialog,
specReference: references[0] ?? null,
generationReferences: [],
}
: {
...dialog,
specReference: null,
generationReferences: references,
};
}
function resetFailedGenerationPanel(panel: QuickEditPanelState) {
return {
...panel,
status: panel.status === 'failed' ? 'idle' : panel.status,
errorMessage: panel.status === 'failed' ? undefined : panel.errorMessage,
};
}
function isSeedanceVideoModel(model: string | undefined) {
const resolvedModel = model ?? DEFAULT_VIDEO_MODEL;
return (
resolvedModel === 'seedance2.0' || resolvedModel === 'seedance2.0-fast'
);
}
function getReferenceMediaType(layer: CanvasLayer) {
return layer.mediaType === 'video' || layer.mediaType === 'audio'
? layer.mediaType
: 'image';
}
export function isIconSpecReferenceLayer(layer: CanvasLayer) {
if (layer.assetKind === 'icon-spec') {
return true;
}
// 中文注释:历史“UI素材规范”图层以普通 spec 保存,但语义上就是图标规范,允许继续作为图标生成参考。
if (layer.assetKind !== 'spec') {
return false;
}
return (
layer.title.includes('图标规范') ||
layer.title.includes('UI素材规范') ||
layer.generationInputs?.fields.some(
(field) =>
formatGenerationInputValue(field.value).includes('游戏UI规范') ||
formatGenerationInputValue(field.value).includes('图标规范'),
) === true
);
}
export function isCharacterSpecReferenceLayer(layer: CanvasLayer) {
if (getReferenceMediaType(layer) !== 'image' || layer.assetKind !== 'spec') {
return false;
}
return !isIconSpecReferenceLayer(layer);
}
function appendLimitedSeedanceCanvasReference(
references: NonNullable<GenerateDialogState['generationReferences']>,
layer: CanvasLayer,
) {
const mediaType = getReferenceMediaType(layer);
const currentCount = references.filter(
(reference) => (reference.mediaType ?? 'image') === mediaType,
).length;
if (currentCount >= VIDEO_REFERENCE_LIMITS[mediaType]) {
return references;
}
return [...references, createCanvasLayerReference(layer)];
}
function resolveImageDimensionDefaults(imageModel: string) {
const normalizedImageModel = normalizeEditorImageModel(imageModel);
const dimensionOptions =
EDITOR_IMAGE_DIMENSION_OPTIONS[
normalizedImageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
return {
aspectRatio: dimensionOptions.aspectRatios[0],
imageSize:
dimensionOptions.imageSizes.find((size) => size === '1K') ??
dimensionOptions.imageSizes[0],
};
}
function getGenerationInputFieldValues(layer: CanvasLayer) {
const values = new Map<string, string>();
for (const field of layer.generationInputs?.fields ?? []) {
const title = field.title.trim().toLowerCase();
const value = formatGenerationInputValue(field.value).trim();
if (title && value && !values.has(title)) {
values.set(title, value);
}
}
return values;
}
export function createGenerateDialogDraft({
canvasSize,
viewport,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
const dimensionDefaults = resolveImageDimensionDefaults(DEFAULT_IMAGE_MODEL);
const placeholderSize = resolveEditorImageGenerationPixelSize({
model: DEFAULT_IMAGE_MODEL,
aspectRatio: dimensionDefaults.aspectRatio,
imageSize: dimensionDefaults.imageSize,
});
return {
mode: 'generate',
prompt: '',
status: 'idle',
composerOpen: true,
style: 'none',
imageModel: DEFAULT_IMAGE_MODEL,
aspectRatio: dimensionDefaults.aspectRatio,
imageSize: dimensionDefaults.imageSize,
placeholder: {
x: worldCenter.x - placeholderSize.width / 2,
y: worldCenter.y - placeholderSize.height / 2,
width: placeholderSize.width,
height: placeholderSize.height,
originalWidth: placeholderSize.width,
originalHeight: placeholderSize.height,
},
};
}
export function createSceneGenerationDialogDraft({
canvasSize,
viewport,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
const imageModel = DEFAULT_IMAGE_MODEL;
const aspectRatio = '16:9';
const imageSize = '1K';
const placeholderSize = resolveEditorImageGenerationPixelSize({
model: imageModel,
aspectRatio,
imageSize,
});
return {
mode: 'scene',
prompt: '',
status: 'idle',
composerOpen: true,
generationReferences: [],
sceneStylePreset: DEFAULT_EDITOR_SCENE_STYLE_PRESET,
sceneCustomStyle: '',
imageModel,
aspectRatio,
imageSize,
placeholder: {
x: worldCenter.x - placeholderSize.width / 2,
y: worldCenter.y - placeholderSize.height / 2,
width: placeholderSize.width,
height: placeholderSize.height,
originalWidth: placeholderSize.width,
originalHeight: placeholderSize.height,
},
};
}
function shouldUseSpecPlaceholderValues(specType: SpecGenerationType) {
return specType === 'character' || specType === 'icon';
}
export function createSpecDialogDraft({
canvasSize,
viewport,
specType,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
specType: SpecGenerationType;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
return {
mode: 'spec',
prompt: '',
status: 'idle',
composerOpen: true,
specType,
specValues: {
...DEFAULT_SPEC_FORM_VALUES[specType],
...(shouldUseSpecPlaceholderValues(specType)
? { playSetting: '', artStyle: '' }
: {}),
},
placeholder: {
x: worldCenter.x - SPEC_FRAME_ORIGINAL_SIZE.width / 2,
y: worldCenter.y - SPEC_FRAME_ORIGINAL_SIZE.height / 2,
width: SPEC_FRAME_ORIGINAL_SIZE.width,
height: SPEC_FRAME_ORIGINAL_SIZE.height,
originalWidth: SPEC_FRAME_ORIGINAL_SIZE.width,
originalHeight: SPEC_FRAME_ORIGINAL_SIZE.height,
},
};
}
export function createCharacterGenerationDialogDraft({
canvasSize,
viewport,
imageModel,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
imageModel: string;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
const normalizedImageModel = normalizeEditorImageModel(imageModel);
const dimensionDefaults = resolveImageDimensionDefaults(normalizedImageModel);
const placeholderSize = resolveEditorImageGenerationPixelSize({
model: normalizedImageModel,
aspectRatio: dimensionDefaults.aspectRatio,
imageSize: dimensionDefaults.imageSize,
});
return {
mode: 'character',
prompt: '',
status: 'idle',
composerOpen: true,
style: 'none',
characterSpecReference: null,
characterReferences: [],
imageModel: normalizedImageModel,
aspectRatio: dimensionDefaults.aspectRatio,
imageSize: dimensionDefaults.imageSize,
placeholder: {
x: worldCenter.x - placeholderSize.width / 2,
y: worldCenter.y - placeholderSize.height / 2,
width: placeholderSize.width,
height: placeholderSize.height,
originalWidth: placeholderSize.width,
originalHeight: placeholderSize.height,
},
};
}
export function createIconGenerationDialogDraft({
canvasSize,
viewport,
imageModel,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
imageModel: string;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
const normalizedImageModel = normalizeEditorImageModel(imageModel);
const dimensionDefaults = resolveImageDimensionDefaults(normalizedImageModel);
const placeholderSize = resolveEditorImageGenerationPixelSize({
model: normalizedImageModel,
aspectRatio: dimensionDefaults.aspectRatio,
imageSize: dimensionDefaults.imageSize,
});
return {
mode: 'icon',
prompt: '',
status: 'idle',
composerOpen: true,
style: 'none',
iconSpecReference: null,
generationReferences: [],
iconDescriptions: [],
imageModel: normalizedImageModel,
aspectRatio: dimensionDefaults.aspectRatio,
imageSize: dimensionDefaults.imageSize,
placeholder: {
x: worldCenter.x - placeholderSize.width / 2,
y: worldCenter.y - placeholderSize.height / 2,
width: placeholderSize.width,
height: placeholderSize.height,
originalWidth: placeholderSize.width,
originalHeight: placeholderSize.height,
},
};
}
export function createPublicationGenerationDialogDraft({
canvasSize,
viewport,
workflowId,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
workflowId: PublicationMaterialsWorkflowId;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
const frameOriginalSize = PUBLICATION_FRAME_ORIGINAL_SIZE[workflowId];
const workflow = getPublicationMaterialsWorkflow(workflowId);
return {
mode: 'publication',
prompt: '',
status: 'idle',
composerOpen: true,
publicationWorkflowId: workflowId,
publicationGameInfo: { ...DEFAULT_PUBLICATION_GAME_INFO },
publicationReferences: [],
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio: workflow.aspectRatio,
imageSize: workflow.imageSize,
placeholder: {
x: worldCenter.x - frameOriginalSize.width / 2,
y: worldCenter.y - frameOriginalSize.height / 2,
width: frameOriginalSize.width,
height: frameOriginalSize.height,
originalWidth: frameOriginalSize.width,
originalHeight: frameOriginalSize.height,
},
};
}
export function createVideoGenerationDialogDraft({
canvasSize,
viewport,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
const placeholderSize = resolveEditorVideoGenerationPixelSize({
aspectRatio: DEFAULT_VIDEO_ASPECT_RATIO,
resolution: '480p',
});
return {
mode: 'video',
prompt: '',
status: 'idle',
composerOpen: true,
generationReferences: [],
videoModel: DEFAULT_VIDEO_MODEL,
videoAspectRatio: DEFAULT_VIDEO_ASPECT_RATIO,
videoResolution: '480p',
videoDurationSeconds: DEFAULT_VIDEO_DURATION_SECONDS,
videoMode: 'std',
videoSound: DEFAULT_VIDEO_SOUND,
videoWebSearchEnabled: DEFAULT_VIDEO_WEB_SEARCH_ENABLED,
placeholder: {
x: worldCenter.x - placeholderSize.width / 2,
y: worldCenter.y - placeholderSize.height / 2,
width: placeholderSize.width,
height: placeholderSize.height,
originalWidth: placeholderSize.width,
originalHeight: placeholderSize.height,
},
};
}
export function createSoundEffectGenerationDialogDraft({
canvasSize,
viewport,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
return {
mode: 'audio-sound-effect',
prompt: '',
status: 'idle',
composerOpen: true,
soundModel: DEFAULT_SOUND_EFFECT_MODEL,
// 与设计稿一致:新建音效默认自动时长,由模型按描述决定长度。
soundDurationMode: 'auto',
soundDurationSeconds: DEFAULT_SOUND_EFFECT_DURATION_SECONDS,
soundLoop: false,
placeholder: {
x: worldCenter.x - AUDIO_FRAME_DISPLAY_SIZE.width / 2,
y: worldCenter.y - AUDIO_FRAME_DISPLAY_SIZE.height / 2,
width: AUDIO_FRAME_DISPLAY_SIZE.width,
height: AUDIO_FRAME_DISPLAY_SIZE.height,
originalWidth: AUDIO_FRAME_ORIGINAL_SIZE.width,
originalHeight: AUDIO_FRAME_ORIGINAL_SIZE.height,
},
};
}
export function createBackgroundMusicGenerationDialogDraft({
canvasSize,
viewport,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
return {
mode: 'audio-background-music',
prompt: '',
status: 'idle',
composerOpen: true,
makeInstrumental: true,
placeholder: {
x: worldCenter.x - AUDIO_FRAME_DISPLAY_SIZE.width / 2,
y: worldCenter.y - AUDIO_FRAME_DISPLAY_SIZE.height / 2,
width: AUDIO_FRAME_DISPLAY_SIZE.width,
height: AUDIO_FRAME_DISPLAY_SIZE.height,
originalWidth: AUDIO_FRAME_ORIGINAL_SIZE.width,
originalHeight: AUDIO_FRAME_ORIGINAL_SIZE.height,
},
};
}
function findGenerationInputFieldValue(
layer: CanvasLayer,
titles: readonly string[],
) {
const normalizedTitles = new Set(titles.map((title) => title.toLowerCase()));
const field = layer.generationInputs?.fields.find((candidate) =>
normalizedTitles.has(candidate.title.trim().toLowerCase()),
);
return field ? formatGenerationInputValue(field.value) : null;
}
const USER_PROMPT_INPUT_TITLES = new Set(
[
'生成提示词',
'视频描述',
'prompt',
'sound',
'gpt_description_prompt',
'音效提示词',
'背景音乐提示词',
'角色设定',
'用户输入',
'素材描述',
'自定义规范提示词',
'修改要求',
'快速编辑提示词',
'重绘提示词',
'动作描述',
'用户描述',
'画面内容',
].map((title) => title.toLowerCase()),
);
const STRUCTURED_USER_INPUT_TITLES = new Set(
[
'玩法设定',
'美术风格',
'头身比',
'角色视角',
'游戏名',
'游戏分类',
'一句话描述游戏',
].map((title) => title.toLowerCase()),
);
function resolveUserGenerationPromptSnapshot(sourceLayer: CanvasLayer) {
const fields = sourceLayer.generationInputs?.fields ?? [];
const promptField = fields.find((field) =>
USER_PROMPT_INPUT_TITLES.has(field.title.trim().toLowerCase()),
);
const promptValue = promptField
? formatGenerationInputValue(promptField.value).trim()
: undefined;
if (promptValue) {
return promptValue;
}
const structuredLines = fields.flatMap((field) => {
if (!STRUCTURED_USER_INPUT_TITLES.has(field.title.trim().toLowerCase())) {
return [];
}
const value = formatGenerationInputValue(field.value).trim();
return value ? [`${field.title.trim()}${value}`] : [];
});
return structuredLines.join('\n');
}
function resolveAudioRedrawPrompt(sourceLayer: CanvasLayer) {
const soundEffect = sourceLayer.generationInputs?.soundEffect;
if (
sourceLayer.assetKind === 'sound-effect' &&
soundEffect?.schemaVersion === 2 &&
soundEffect.userPrompt.trim()
) {
return soundEffect.userPrompt;
}
return resolveUserGenerationPromptSnapshot(sourceLayer);
}
function resolveAudioRedrawMode(sourceLayer: CanvasLayer) {
if (sourceLayer.assetKind === 'background-music') {
return 'audio-background-music' as const;
}
if (sourceLayer.assetKind === 'sound-effect') {
return 'audio-sound-effect' as const;
}
if (sourceLayer.mediaType !== 'audio') {
return null;
}
return sourceLayer.title.includes('背景音乐')
? ('audio-background-music' as const)
: ('audio-sound-effect' as const);
}
function resolveAudioRedrawSoundModel(
_sourceLayer: CanvasLayer,
): NonNullable<GenerateDialogState['soundModel']> {
// 历史 Vidu 素材只提供用户 Prompt 与可兼容的手动时长;重绘始终打开 SFX V2。
return DEFAULT_SOUND_EFFECT_MODEL;
}
function resolveAudioRedrawSoundDuration(sourceLayer: CanvasLayer) {
const soundEffect = sourceLayer.generationInputs?.soundEffect;
if (soundEffect?.schemaVersion === 2) {
if (
soundEffect.durationMode === 'manual' &&
typeof soundEffect.requestedDurationSeconds === 'number' &&
Number.isFinite(soundEffect.requestedDurationSeconds)
) {
return soundEffect.requestedDurationSeconds;
}
return DEFAULT_SOUND_EFFECT_DURATION_SECONDS;
}
const fieldValue = findGenerationInputFieldValue(sourceLayer, [
'时长',
'duration',
]);
const matchedValue = fieldValue?.match(/\d+(?:\.\d+)?/u)?.[0];
const duration = matchedValue ? Number.parseFloat(matchedValue) : null;
if (!duration || !Number.isFinite(duration)) {
return DEFAULT_SOUND_EFFECT_DURATION_SECONDS;
}
return Math.min(
SOUND_EFFECT_DURATION_MAX_SECONDS,
Math.max(SOUND_EFFECT_DURATION_MIN_SECONDS, duration),
);
}
export function createAudioRedrawGenerationDialogDraft(
sourceLayer: CanvasLayer,
): Omit<CanvasGenerationDialogState, 'id'> | null {
const mode = resolveAudioRedrawMode(sourceLayer);
if (!mode) {
return null;
}
const baseDraft = {
mode,
sourceLayerId: sourceLayer.id,
prompt: resolveAudioRedrawPrompt(sourceLayer),
status: 'idle' as const,
composerOpen: true,
placeholder: {
x: sourceLayer.x + sourceLayer.width + 32,
y: sourceLayer.y,
width: AUDIO_FRAME_DISPLAY_SIZE.width,
height: AUDIO_FRAME_DISPLAY_SIZE.height,
originalWidth: AUDIO_FRAME_ORIGINAL_SIZE.width,
originalHeight: AUDIO_FRAME_ORIGINAL_SIZE.height,
},
};
if (mode === 'audio-background-music') {
return {
...baseDraft,
makeInstrumental: true,
};
}
return {
...baseDraft,
soundModel: resolveAudioRedrawSoundModel(sourceLayer),
soundDurationMode:
sourceLayer.generationInputs?.soundEffect?.schemaVersion === 2 &&
sourceLayer.generationInputs.soundEffect.durationMode === 'auto'
? 'auto'
: 'manual',
soundDurationSeconds: resolveAudioRedrawSoundDuration(sourceLayer),
soundLoop:
sourceLayer.generationInputs?.soundEffect?.schemaVersion === 2
? sourceLayer.generationInputs.soundEffect.loop
: false,
};
}
export function createVideoRedrawGenerationDialogDraft(
sourceLayer: CanvasLayer,
): Omit<CanvasGenerationDialogState, 'id'> | null {
if (sourceLayer.mediaType !== 'video') {
return null;
}
const placeholderSize = resolveEditorVideoGenerationPixelSize({
aspectRatio: DEFAULT_VIDEO_ASPECT_RATIO,
resolution: '480p',
});
return {
mode: 'video',
sourceLayerId: sourceLayer.id,
prompt: resolveUserGenerationPromptSnapshot(sourceLayer),
status: 'idle',
composerOpen: true,
generationReferences: [createCanvasLayerReference(sourceLayer)],
videoModel: DEFAULT_VIDEO_MODEL,
videoAspectRatio: DEFAULT_VIDEO_ASPECT_RATIO,
videoResolution: '480p',
videoDurationSeconds: DEFAULT_VIDEO_DURATION_SECONDS,
videoMode: 'std',
videoSound: DEFAULT_VIDEO_SOUND,
videoWebSearchEnabled: DEFAULT_VIDEO_WEB_SEARCH_ENABLED,
placeholder: {
x: sourceLayer.x + sourceLayer.width + 32,
y: sourceLayer.y,
width: placeholderSize.width,
height: placeholderSize.height,
originalWidth: placeholderSize.width,
originalHeight: placeholderSize.height,
},
};
}
function resolveGeneratedSourceDialogMode({
sourceLayer,
sourceDialog,
}: {
sourceLayer: CanvasLayer;
sourceDialog?: CanvasGenerationDialogState | null;
}): CanvasGenerationDialogState['mode'] | null {
if (sourceDialog) {
return sourceDialog.mode;
}
if (sourceLayer.assetKind === 'character') {
return 'character';
}
if (sourceLayer.assetKind === 'scene') {
return 'scene';
}
if (sourceLayer.assetKind === 'ui-design') {
return 'ui-design';
}
if (sourceLayer.assetKind === 'publication-material') {
return 'publication';
}
if (
sourceLayer.assetKind === 'icon' ||
sourceLayer.assetKind === 'icon-spritesheet'
) {
return 'icon';
}
if (
sourceLayer.assetKind === 'spec' ||
sourceLayer.assetKind === 'icon-spec'
) {
return 'spec';
}
if (sourceLayer.assetKind === 'character-animation') {
return 'character-animation';
}
if (sourceLayer.assetKind === 'video' || sourceLayer.mediaType === 'video') {
return 'video';
}
if (sourceLayer.assetKind === 'sound-effect') {
return 'audio-sound-effect';
}
if (sourceLayer.assetKind === 'background-music') {
return 'audio-background-music';
}
if (sourceLayer.mediaType === 'audio') {
return resolveAudioRedrawMode(sourceLayer);
}
if (sourceLayer.sourceType === 'generated') {
return 'generate';
}
return null;
}
function buildLayerGenerationPlaceholder(sourceLayer: CanvasLayer) {
return {
x: sourceLayer.x,
y: sourceLayer.y,
width: sourceLayer.width,
height: sourceLayer.height,
originalWidth: sourceLayer.originalWidth,
originalHeight: sourceLayer.originalHeight,
};
}
export function createLayerGenerationDialogDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
sourceAnimationLayer,
availableLayers,
}: Omit<SourceGenerationDialogDraftContext, 'mode'>): Omit<
CanvasGenerationDialogState,
'id'
> | null {
const draft = createSameSourceGenerationDialogDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
sourceAnimationLayer,
availableLayers,
mode: 'redraw',
});
if (!draft) {
return null;
}
const usesNormalizedInputs = isNormalizedCanvasGenerationInputs(
sourceLayer.generationInputs,
);
return {
...draft,
prompt: usesNormalizedInputs
? draft.prompt
: draft.prompt || sourceLayer.prompt?.trim() || '',
imageModel: usesNormalizedInputs
? draft.imageModel
: (sourceDialog?.imageModel ?? draft.imageModel),
status: 'idle',
composerOpen: true,
generatedLayerId: sourceLayer.id,
placeholder: buildLayerGenerationPlaceholder(sourceLayer),
};
}
function getSourceDraftPrompt({
sourceLayer,
mode,
}: {
sourceLayer: CanvasLayer;
mode: SourceGenerationDialogDraftContext['mode'];
}) {
if (mode === 'quick-edit') {
return '';
}
return resolveUserGenerationPromptSnapshot(sourceLayer);
}
function buildSourceSidePlaceholder(sourceLayer: CanvasLayer) {
return {
x: sourceLayer.x + sourceLayer.width + 32,
y: sourceLayer.y,
};
}
function placeDraftBesideSourceLayer<
T extends Omit<CanvasGenerationDialogState, 'id'>,
>(draft: T, sourceLayer: CanvasLayer): T {
if (!draft.placeholder) {
return draft;
}
return {
...draft,
placeholder: {
...draft.placeholder,
...buildSourceSidePlaceholder(sourceLayer),
},
};
}
function getNormalizedFieldValue(sourceLayer: CanvasLayer, id: string) {
return sourceLayer.generationInputs?.fields.find((field) => field.id === id)
?.value;
}
function getNormalizedString(
sourceLayer: CanvasLayer,
id: string,
fallback = '',
) {
const value = getNormalizedFieldValue(sourceLayer, id);
return typeof value === 'string' ? value : fallback;
}
function getNormalizedNumber(
sourceLayer: CanvasLayer,
id: string,
fallback: number,
) {
const value = getNormalizedFieldValue(sourceLayer, id);
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
}
function getNormalizedBoolean(
sourceLayer: CanvasLayer,
id: string,
fallback: boolean,
) {
const value = getNormalizedFieldValue(sourceLayer, id);
return typeof value === 'boolean' ? value : fallback;
}
function findAvailableGenerationReferenceLayer(
reference: CanvasGenerationInputReference,
availableLayers: CanvasLayer[] | undefined,
) {
return (availableLayers ?? []).find((candidate) =>
reference.refType === 'project-resource'
? candidate.resourceId === reference.refId
: candidate.sourceAssetId === reference.refId,
);
}
export function getUnavailableNormalizedGenerationInputReferences(
sourceLayer: CanvasLayer,
availableLayers: CanvasLayer[] | undefined,
) {
const decodedInputs = decodeCanvasGenerationInputs(
sourceLayer.generationInputs,
);
if (!decodedInputs.ok) {
return [];
}
return decodedInputs.inputs.references.filter(
(reference) =>
!findAvailableGenerationReferenceLayer(reference, availableLayers),
);
}
function resolveNormalizedReferences(
sourceLayer: CanvasLayer,
id: string,
availableLayers: CanvasLayer[] | undefined,
) {
return (sourceLayer.generationInputs?.references ?? [])
.filter((reference) => reference.id === id)
.flatMap((reference) => {
const layer = findAvailableGenerationReferenceLayer(
reference,
availableLayers,
);
return layer ? [createCanvasLayerReference(layer)] : [];
});
}
function createNormalizedGenerationDialogDraft({
sourceLayer: persistedSourceLayer,
canvasSize,
viewport,
availableLayers,
}: SourceGenerationDialogDraftContext): Omit<
CanvasGenerationDialogState,
'id'
> | null {
const decodedInputs = decodeCanvasGenerationInputs(
persistedSourceLayer.generationInputs,
);
if (!decodedInputs.ok) {
return null;
}
const inputs = decodedInputs.inputs;
const sourceLayer: CanvasLayer = {
...persistedSourceLayer,
generationInputs: inputs,
};
const prompt = getNormalizedString(sourceLayer, 'prompt');
const references = resolveNormalizedReferences(
sourceLayer,
'reference',
availableLayers,
);
if (inputs.action === 'image.generate') {
return placeDraftBesideSourceLayer(
resizeGenerationPlaceholderToImageSelection({
...createGenerateDialogDraft({ canvasSize, viewport }),
prompt,
sourceLayerId: sourceLayer.id,
imageModel: getNormalizedString(
sourceLayer,
'model',
DEFAULT_IMAGE_MODEL,
),
style:
getNormalizedString(sourceLayer, 'style') === 'pixelArt'
? 'pixelArt'
: 'none',
aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '1:1'),
imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'),
generationReferences: references,
}),
sourceLayer,
);
}
if (inputs.action === 'scene.generate') {
return placeDraftBesideSourceLayer(
resizeGenerationPlaceholderToImageSelection({
...createSceneGenerationDialogDraft({ canvasSize, viewport }),
prompt,
sourceLayerId: sourceLayer.id,
sceneStylePreset: getNormalizedString(
sourceLayer,
'stylePreset',
DEFAULT_EDITOR_SCENE_STYLE_PRESET,
) as GenerateDialogState['sceneStylePreset'],
sceneCustomStyle: getNormalizedString(sourceLayer, 'customStyle'),
imageModel: getNormalizedString(
sourceLayer,
'model',
DEFAULT_IMAGE_MODEL,
),
aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '16:9'),
imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'),
generationReferences: references,
}),
sourceLayer,
);
}
if (inputs.action === 'spec.generate') {
const specTypeValue = getNormalizedString(
sourceLayer,
'specType',
'custom',
);
const specType: SpecGenerationType =
specTypeValue === 'ui'
? 'icon'
: specTypeValue === 'character' || specTypeValue === 'icon'
? specTypeValue
: 'custom';
return placeDraftBesideSourceLayer(
{
...createSpecDialogDraft({ canvasSize, viewport, specType }),
sourceLayerId: sourceLayer.id,
specValues: {
playSetting: getNormalizedString(sourceLayer, 'playSetting'),
artStyle: getNormalizedString(sourceLayer, 'artStyle'),
bodyRatio: getNormalizedString(sourceLayer, 'bodyRatio'),
characterView: getNormalizedString(sourceLayer, 'characterView'),
customPrompt: getNormalizedString(sourceLayer, 'customPrompt'),
},
...(specType === 'icon'
? {
specReference:
resolveNormalizedReferences(
sourceLayer,
'reference',
availableLayers,
)[0] ?? null,
}
: {
generationReferences: resolveNormalizedReferences(
sourceLayer,
'reference',
availableLayers,
),
}),
},
sourceLayer,
);
}
if (inputs.action === 'character.generate') {
return placeDraftBesideSourceLayer(
resizeGenerationPlaceholderToImageSelection({
...createCharacterGenerationDialogDraft({
canvasSize,
viewport,
imageModel: getNormalizedString(
sourceLayer,
'model',
DEFAULT_IMAGE_MODEL,
),
}),
prompt,
sourceLayerId: sourceLayer.id,
style:
getNormalizedString(sourceLayer, 'style') === 'pixelArt'
? 'pixelArt'
: 'none',
aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '1:1'),
imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'),
characterSpecReference:
resolveNormalizedReferences(
sourceLayer,
'specReference',
availableLayers,
)[0] ?? null,
characterReferences: references,
}),
sourceLayer,
);
}
if (inputs.action === 'icon.generate') {
return placeDraftBesideSourceLayer(
resizeGenerationPlaceholderToImageSelection({
...createIconGenerationDialogDraft({
canvasSize,
viewport,
imageModel: getNormalizedString(
sourceLayer,
'model',
DEFAULT_IMAGE_MODEL,
),
}),
prompt,
sourceLayerId: sourceLayer.id,
style:
getNormalizedString(sourceLayer, 'style') === 'pixelArt'
? 'pixelArt'
: 'none',
aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '1:1'),
imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'),
iconDescriptions: prompt ? [prompt] : [],
iconSpecReference:
resolveNormalizedReferences(
sourceLayer,
'specReference',
availableLayers,
)[0] ?? null,
generationReferences: references,
}),
sourceLayer,
);
}
if (inputs.action === 'ui-design.generate') {
return placeDraftBesideSourceLayer(
resizeGenerationPlaceholderToImageSelection({
...createUiDesignGenerationDialogDraft({
canvasSize,
viewport,
imageModel: getNormalizedString(
sourceLayer,
'model',
IMAGE_MODEL_GPT_IMAGE_2,
),
}),
prompt,
sourceLayerId: sourceLayer.id,
aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '16:9'),
imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'),
uiDesignSpecReference:
resolveNormalizedReferences(
sourceLayer,
'specReference',
availableLayers,
)[0] ?? null,
generationReferences: references,
}),
sourceLayer,
);
}
if (inputs.action === 'publication.generate') {
const workflowValue = getNormalizedString(
sourceLayer,
'workflowId',
'publication-cover-image',
);
const workflowId: PublicationMaterialsWorkflowId =
workflowValue === 'publication-detail-gallery' ||
workflowValue === 'publication-promo-poster'
? workflowValue
: 'publication-cover-image';
return placeDraftBesideSourceLayer(
{
...createPublicationGenerationDialogDraft({
canvasSize,
viewport,
workflowId,
}),
sourceLayerId: sourceLayer.id,
publicationGameInfo: {
gameName: getNormalizedString(sourceLayer, 'gameName'),
gameCategories: getNormalizedString(sourceLayer, 'gameCategories'),
gameDescription: getNormalizedString(sourceLayer, 'gameDescription'),
},
publicationReferences: references,
},
sourceLayer,
);
}
if (inputs.action === 'video.generate') {
const videoDraft = resizeGenerationPlaceholderToVideoSelection({
...createVideoGenerationDialogDraft({ canvasSize, viewport }),
prompt,
sourceLayerId: sourceLayer.id,
videoModel: getNormalizedString(
sourceLayer,
'model',
DEFAULT_VIDEO_MODEL,
) as GenerateDialogState['videoModel'],
videoAspectRatio: getNormalizedString(
sourceLayer,
'aspectRatio',
DEFAULT_VIDEO_ASPECT_RATIO,
) as GenerateDialogState['videoAspectRatio'],
videoResolution: getNormalizedString(
sourceLayer,
'resolution',
'480p',
) as GenerateDialogState['videoResolution'],
videoDurationSeconds: getNormalizedNumber(
sourceLayer,
'durationSeconds',
DEFAULT_VIDEO_DURATION_SECONDS,
),
videoSound: getNormalizedString(
sourceLayer,
'sound',
DEFAULT_VIDEO_SOUND,
) as GenerateDialogState['videoSound'],
videoWebSearchEnabled: getNormalizedBoolean(
sourceLayer,
'webSearchEnabled',
DEFAULT_VIDEO_WEB_SEARCH_ENABLED,
),
generationReferences: [
...resolveNormalizedReferences(
sourceLayer,
'imageReference',
availableLayers,
),
...resolveNormalizedReferences(
sourceLayer,
'videoReference',
availableLayers,
),
...resolveNormalizedReferences(
sourceLayer,
'audioReference',
availableLayers,
),
],
}) as Omit<CanvasGenerationDialogState, 'id'>;
return placeDraftBesideSourceLayer(videoDraft, sourceLayer);
}
if (
inputs.action === 'audio.sound-effect.generate' ||
inputs.action === 'audio.background-music.generate'
) {
const soundEffect = persistedSourceLayer.generationInputs?.soundEffect;
const restoredDurationMode =
soundEffect?.schemaVersion === 2
? soundEffect.durationMode
: getNormalizedString(sourceLayer, 'durationMode', 'manual');
return placeDraftBesideSourceLayer(
{
...(inputs.action === 'audio.sound-effect.generate'
? createSoundEffectGenerationDialogDraft({ canvasSize, viewport })
: createBackgroundMusicGenerationDialogDraft({
canvasSize,
viewport,
})),
prompt,
sourceLayerId: sourceLayer.id,
soundModel:
inputs.action === 'audio.sound-effect.generate'
? (getNormalizedString(
sourceLayer,
'model',
DEFAULT_SOUND_EFFECT_MODEL,
) as GenerateDialogState['soundModel'])
: undefined,
soundDurationSeconds:
inputs.action === 'audio.sound-effect.generate'
? soundEffect?.schemaVersion === 2 &&
soundEffect.durationMode === 'manual' &&
typeof soundEffect.requestedDurationSeconds === 'number'
? soundEffect.requestedDurationSeconds
: getNormalizedNumber(
sourceLayer,
'durationSeconds',
DEFAULT_SOUND_EFFECT_DURATION_SECONDS,
)
: undefined,
soundDurationMode:
inputs.action === 'audio.sound-effect.generate'
? restoredDurationMode === 'auto'
? 'auto'
: 'manual'
: undefined,
soundLoop:
inputs.action === 'audio.sound-effect.generate'
? soundEffect?.schemaVersion === 2
? soundEffect.loop
: getNormalizedBoolean(sourceLayer, 'loop', false)
: undefined,
},
sourceLayer,
);
}
if (inputs.action === 'character-animation.generate') {
const animationSource = resolveNormalizedReferences(
sourceLayer,
'source',
availableLayers,
)
.map((reference) =>
(availableLayers ?? []).find(
(layer) => createCanvasLayerReference(layer).id === reference.id,
),
)
.find((layer): layer is CanvasLayer => Boolean(layer));
if (!animationSource) {
return null;
}
const draft = createCharacterAnimationGenerationDialogDraft({
canvasSize,
viewport,
layer: animationSource,
});
return draft
? placeDraftBesideSourceLayer(
{
...draft,
prompt,
sourceLayerId: animationSource.id,
characterAnimationResolution: getNormalizedString(
sourceLayer,
'resolution',
'480p',
) as GenerateDialogState['characterAnimationResolution'],
characterAnimationRatio: getNormalizedString(
sourceLayer,
'ratio',
'same',
) as GenerateDialogState['characterAnimationRatio'],
characterAnimationFrameCount: getNormalizedNumber(
sourceLayer,
'frameCount',
32,
) as GenerateDialogState['characterAnimationFrameCount'],
characterAnimationDurationSeconds: getNormalizedNumber(
sourceLayer,
'durationSeconds',
4,
) as GenerateDialogState['characterAnimationDurationSeconds'],
},
sourceLayer,
)
: null;
}
return null;
}
function restoreSharedImageOptions(
draft: Omit<CanvasGenerationDialogState, 'id'>,
sourceLayer: CanvasLayer,
sourceDialog?: CanvasGenerationDialogState | null,
): Omit<CanvasGenerationDialogState, 'id'> {
const requestedModel = normalizeEditorImageModel(
sourceDialog?.imageModel ?? sourceLayer.model ?? draft.imageModel,
);
const imageModel = EDITOR_IMAGE_MODEL_OPTIONS.some(
(option) => option.value === requestedModel,
)
? requestedModel
: DEFAULT_IMAGE_MODEL;
const dimensionOptions =
EDITOR_IMAGE_DIMENSION_OPTIONS[
imageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
const supportedAspectRatios =
dimensionOptions.aspectRatios as readonly string[];
const supportedImageSizes = dimensionOptions.imageSizes as readonly string[];
const inferredAspectRatio = inferEditorImageAspectRatio(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
const inferredImageSize = inferEditorImageSizeLabel(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
const preferredAspectRatio = sourceDialog?.aspectRatio ?? inferredAspectRatio;
const preferredImageSize = sourceDialog?.imageSize ?? inferredImageSize;
return resizeGenerationPlaceholderToImageSelection({
...draft,
imageModel,
aspectRatio: supportedAspectRatios.includes(preferredAspectRatio)
? preferredAspectRatio
: draft.aspectRatio && supportedAspectRatios.includes(draft.aspectRatio)
? draft.aspectRatio
: (dimensionOptions.aspectRatios[0] ?? '1:1'),
imageSize: supportedImageSizes.includes(preferredImageSize)
? preferredImageSize
: draft.imageSize && supportedImageSizes.includes(draft.imageSize)
? draft.imageSize
: (dimensionOptions.imageSizes.find((size) => size === '1K') ??
dimensionOptions.imageSizes[0] ??
'1K'),
});
}
function resolveSpecTypeFromSourceLayer(
sourceLayer: CanvasLayer,
sourceDialog?: CanvasGenerationDialogState | null,
): SpecGenerationType {
if (sourceDialog?.mode === 'spec' && sourceDialog.specType) {
return sourceDialog.specType;
}
if (
sourceLayer.assetKind === 'icon-spec' ||
isIconSpecReferenceLayer(sourceLayer)
) {
return 'icon';
}
const fields = getGenerationInputFieldValues(sourceLayer);
if (fields.has('自定义规范提示词')) {
return 'custom';
}
if (fields.has('头身比') || fields.has('角色视角')) {
return 'character';
}
return 'icon';
}
function restoreSpecValuesFromLayer(
draft: Omit<CanvasGenerationDialogState, 'id'>,
sourceLayer: CanvasLayer,
specType: SpecGenerationType,
sourceDialog?: CanvasGenerationDialogState | null,
): Omit<CanvasGenerationDialogState, 'id'> {
const fields = getGenerationInputFieldValues(sourceLayer);
const defaultValues = DEFAULT_SPEC_FORM_VALUES[specType];
return {
...draft,
specValues: {
...defaultValues,
...sourceDialog?.specValues,
playSetting:
fields.get('玩法设定') ??
sourceDialog?.specValues?.playSetting ??
defaultValues.playSetting,
artStyle:
fields.get('美术风格') ??
sourceDialog?.specValues?.artStyle ??
defaultValues.artStyle,
bodyRatio:
fields.get('头身比') ??
sourceDialog?.specValues?.bodyRatio ??
defaultValues.bodyRatio,
characterView:
fields.get('角色视角') ??
sourceDialog?.specValues?.characterView ??
defaultValues.characterView,
customPrompt:
fields.get('自定义规范提示词') ??
sourceDialog?.specValues?.customPrompt ??
defaultValues.customPrompt,
},
};
}
function resolvePublicationWorkflowIdFromLayer(
sourceLayer: CanvasLayer,
sourceDialog?: CanvasGenerationDialogState | null,
): PublicationMaterialsWorkflowId {
if (
sourceDialog?.mode === 'publication' &&
sourceDialog.publicationWorkflowId
) {
return sourceDialog.publicationWorkflowId;
}
const ratio = inferEditorImageAspectRatio(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
if (ratio === '9:16') {
return 'publication-detail-gallery';
}
if (ratio === '16:9') {
return 'publication-promo-poster';
}
return 'publication-cover-image';
}
function restorePublicationGameInfoFromLayer(
draft: Omit<CanvasGenerationDialogState, 'id'>,
sourceLayer: CanvasLayer,
sourceDialog?: CanvasGenerationDialogState | null,
): Omit<CanvasGenerationDialogState, 'id'> {
const fields = getGenerationInputFieldValues(sourceLayer);
return {
...draft,
publicationGameInfo: {
...DEFAULT_PUBLICATION_GAME_INFO,
...sourceDialog?.publicationGameInfo,
gameName:
fields.get('游戏名') ??
sourceDialog?.publicationGameInfo?.gameName ??
DEFAULT_PUBLICATION_GAME_INFO.gameName,
gameCategories:
fields.get('游戏分类') ??
sourceDialog?.publicationGameInfo?.gameCategories ??
DEFAULT_PUBLICATION_GAME_INFO.gameCategories,
gameDescription:
fields.get('一句话描述游戏') ??
sourceDialog?.publicationGameInfo?.gameDescription ??
DEFAULT_PUBLICATION_GAME_INFO.gameDescription,
},
publicationReferences: sourceDialog?.publicationReferences ?? [],
};
}
function restoreIconDescriptionsFromLayer(
draft: Omit<CanvasGenerationDialogState, 'id'>,
sourceLayer: CanvasLayer,
sourceDialog?: CanvasGenerationDialogState | null,
): Omit<CanvasGenerationDialogState, 'id'> {
const prompt =
findGenerationInputFieldValue(sourceLayer, ['素材描述']) ??
(sourceDialog?.prompt || sourceDialog?.iconDescriptions?.join('\n')) ??
'';
const normalizedPrompt = prompt.trim();
return {
...draft,
prompt,
iconDescriptions: normalizedPrompt
? [normalizedPrompt]
: (sourceDialog?.iconDescriptions ?? draft.iconDescriptions),
iconSpecReference:
sourceDialog?.iconSpecReference ?? draft.iconSpecReference,
generationReferences: sourceDialog?.generationReferences ?? [],
};
}
function createSameSourceCharacterAnimationDraft({
sourceLayer,
canvasSize,
viewport,
sourceAnimationLayer,
mode,
}: SourceGenerationDialogDraftContext): Omit<
CanvasGenerationDialogState,
'id'
> | null {
const animationSourceLayer = sourceAnimationLayer ?? sourceLayer;
const draft = createCharacterAnimationGenerationDialogDraft({
canvasSize,
viewport,
layer: animationSourceLayer,
});
if (!draft) {
return null;
}
return placeDraftBesideSourceLayer(
{
...draft,
prompt: getSourceDraftPrompt({ sourceLayer, mode }),
sourceLayerId: animationSourceLayer.id,
composerOpen: true,
},
sourceLayer,
);
}
export function createSameSourceGenerationDialogDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
mode,
sourceAnimationLayer,
availableLayers,
}: SourceGenerationDialogDraftContext): Omit<
CanvasGenerationDialogState,
'id'
> | null {
if (sourceLayer.generationInputsHydrationState === 'invalid-versioned') {
return null;
}
const normalizedDraft = createNormalizedGenerationDialogDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
mode,
sourceAnimationLayer,
availableLayers,
});
if (normalizedDraft) {
return normalizedDraft;
}
if (isNormalizedCanvasGenerationInputs(sourceLayer.generationInputs)) {
return null;
}
const sourceMode = resolveGeneratedSourceDialogMode({
sourceLayer,
sourceDialog,
});
const prompt = getSourceDraftPrompt({ sourceLayer, mode });
if (sourceMode === 'quick-edit') {
return null;
}
if (sourceMode === 'generate') {
return placeDraftBesideSourceLayer(
restoreSharedImageOptions(
{
...createGenerateDialogDraft({ canvasSize, viewport }),
prompt,
sourceLayerId: sourceLayer.id,
generationReferences:
sourceDialog?.mode === 'generate'
? (sourceDialog.generationReferences ?? [])
: [],
},
sourceLayer,
sourceDialog,
),
sourceLayer,
);
}
if (sourceMode === 'scene') {
const fields = getGenerationInputFieldValues(sourceLayer);
const styleLabel = fields.get('视觉风格');
const sceneStylePreset =
sourceDialog?.sceneStylePreset ??
resolveEditorSceneStylePresetByLabel(styleLabel);
return placeDraftBesideSourceLayer(
restoreSharedImageOptions(
{
...createSceneGenerationDialogDraft({ canvasSize, viewport }),
prompt,
sourceLayerId: sourceLayer.id,
sceneStylePreset,
sceneCustomStyle:
fields.get('自定义画风') ?? sourceDialog?.sceneCustomStyle ?? '',
generationReferences:
sourceDialog?.mode === 'scene'
? (sourceDialog.generationReferences ?? [])
: [],
},
sourceLayer,
sourceDialog,
),
sourceLayer,
);
}
if (sourceMode === 'video') {
const draft = createVideoRedrawGenerationDialogDraft(sourceLayer);
if (!draft) {
return null;
}
return {
...draft,
prompt,
videoModel: sourceDialog?.videoModel ?? draft.videoModel,
videoAspectRatio:
sourceDialog?.videoAspectRatio ?? draft.videoAspectRatio,
videoResolution: sourceDialog?.videoResolution ?? draft.videoResolution,
videoDurationSeconds:
sourceDialog?.videoDurationSeconds ?? draft.videoDurationSeconds,
videoMode: sourceDialog?.videoMode ?? draft.videoMode,
videoSound: sourceDialog?.videoSound ?? draft.videoSound,
videoWebSearchEnabled:
sourceDialog?.videoWebSearchEnabled ?? draft.videoWebSearchEnabled,
generationReferences:
sourceDialog?.mode === 'video'
? (sourceDialog.generationReferences ?? draft.generationReferences)
: draft.generationReferences,
};
}
if (
sourceMode === 'audio-sound-effect' ||
sourceMode === 'audio-background-music'
) {
const draft = createAudioRedrawGenerationDialogDraft(sourceLayer);
if (!draft) {
return null;
}
return {
...draft,
prompt,
soundModel: sourceDialog?.soundModel ?? draft.soundModel,
soundDurationMode:
sourceDialog?.soundDurationMode ?? draft.soundDurationMode,
soundDurationSeconds:
sourceDialog?.soundDurationSeconds ?? draft.soundDurationSeconds,
soundLoop: sourceDialog?.soundLoop ?? draft.soundLoop,
makeInstrumental:
sourceDialog?.makeInstrumental ?? draft.makeInstrumental,
};
}
if (sourceMode === 'character-animation') {
return createSameSourceCharacterAnimationDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
mode,
sourceAnimationLayer,
});
}
if (sourceMode === 'character') {
return placeDraftBesideSourceLayer(
restoreSharedImageOptions(
{
...createCharacterGenerationDialogDraft({
canvasSize,
viewport,
imageModel:
sourceDialog?.imageModel ??
sourceLayer.model ??
DEFAULT_IMAGE_MODEL,
}),
prompt,
sourceLayerId: sourceLayer.id,
characterSpecReference:
sourceDialog?.mode === 'character'
? sourceDialog.characterSpecReference
: null,
characterReferences:
sourceDialog?.mode === 'character'
? (sourceDialog.characterReferences ?? [])
: [],
},
sourceLayer,
sourceDialog,
),
sourceLayer,
);
}
if (sourceMode === 'ui-design') {
return placeDraftBesideSourceLayer(
restoreSharedImageOptions(
{
...createUiDesignGenerationDialogDraft({
canvasSize,
viewport,
imageModel: sourceDialog?.imageModel ?? IMAGE_MODEL_GPT_IMAGE_2,
}),
prompt,
sourceLayerId: sourceLayer.id,
uiDesignSpecReference:
sourceDialog?.mode === 'ui-design'
? sourceDialog.uiDesignSpecReference
: null,
},
sourceLayer,
sourceDialog,
),
sourceLayer,
);
}
if (sourceMode === 'publication') {
const workflowId = resolvePublicationWorkflowIdFromLayer(
sourceLayer,
sourceDialog,
);
return placeDraftBesideSourceLayer(
restorePublicationGameInfoFromLayer(
{
...createPublicationGenerationDialogDraft({
canvasSize,
viewport,
workflowId,
}),
prompt,
sourceLayerId: sourceLayer.id,
},
sourceLayer,
sourceDialog,
),
sourceLayer,
);
}
if (sourceMode === 'icon') {
return placeDraftBesideSourceLayer(
restoreSharedImageOptions(
restoreIconDescriptionsFromLayer(
{
...createIconGenerationDialogDraft({
canvasSize,
viewport,
imageModel:
sourceDialog?.imageModel ??
sourceLayer.model ??
DEFAULT_IMAGE_MODEL,
}),
sourceLayerId: sourceLayer.id,
},
sourceLayer,
sourceDialog,
),
sourceLayer,
sourceDialog,
),
sourceLayer,
);
}
if (sourceMode === 'spec') {
const specType = resolveSpecTypeFromSourceLayer(sourceLayer, sourceDialog);
const sourceReferences =
sourceDialog?.mode === 'spec'
? resolveSpecGenerationReferences(sourceDialog)
: [];
return placeDraftBesideSourceLayer(
restoreSpecValuesFromLayer(
{
...createSpecDialogDraft({ canvasSize, viewport, specType }),
prompt,
sourceLayerId: sourceLayer.id,
...(specType === 'icon'
? {
specReference: sourceReferences[0] ?? null,
generationReferences: [],
}
: {
specReference: null,
generationReferences: sourceReferences,
}),
},
sourceLayer,
specType,
sourceDialog,
),
sourceLayer,
);
}
return null;
}
export function createUiDesignGenerationDialogDraft({
canvasSize,
viewport,
imageModel,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
imageModel: string;
}): Omit<CanvasGenerationDialogState, 'id'> {
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
const normalizedImageModel = normalizeEditorImageModel(imageModel);
const dimensionDefaults = resolveImageDimensionDefaults(normalizedImageModel);
const placeholderSize = resolveEditorImageGenerationPixelSize({
model: normalizedImageModel,
aspectRatio: '16:9',
imageSize: dimensionDefaults.imageSize,
});
return {
mode: 'ui-design',
prompt: '',
status: 'idle',
composerOpen: true,
uiDesignSpecReference: null,
imageModel: normalizedImageModel,
aspectRatio: '16:9',
imageSize: dimensionDefaults.imageSize,
placeholder: {
x: worldCenter.x - placeholderSize.width / 2,
y: worldCenter.y - placeholderSize.height / 2,
width: placeholderSize.width,
height: placeholderSize.height,
originalWidth: placeholderSize.width,
originalHeight: placeholderSize.height,
},
};
}
export function createEditDialogDraft(
sourceLayer: CanvasLayer,
options: {
imageModel?: string;
aspectRatio?: string;
imageSize?: string;
} = {},
): GenerateDialogState {
const decodedInputs = decodeCanvasGenerationInputs(
sourceLayer.generationInputs,
);
const normalizedSourceLayer = decodedInputs.ok
? { ...sourceLayer, generationInputs: decodedInputs.inputs }
: sourceLayer;
const persistedImageModel = decodedInputs.ok
? getNormalizedString(normalizedSourceLayer, 'model') || undefined
: undefined;
const persistedAspectRatio = decodedInputs.ok
? getNormalizedString(normalizedSourceLayer, 'aspectRatio') || undefined
: undefined;
const persistedImageSize = decodedInputs.ok
? getNormalizedString(normalizedSourceLayer, 'imageSize') || undefined
: undefined;
const resolvedOptions = createQuickEditPanelDraft(sourceLayer, {
imageModel:
options.imageModel ??
persistedImageModel ??
sourceLayer.model ??
undefined,
aspectRatio: options.aspectRatio ?? persistedAspectRatio,
imageSize: options.imageSize ?? persistedImageSize,
});
return {
id: `edit-dialog:${sourceLayer.id}`,
mode: 'edit',
prompt: resolveUserGenerationPromptSnapshot(sourceLayer),
status: 'idle',
composerOpen: true,
sourceLayerId: sourceLayer.id,
imageModel: resolvedOptions.model,
aspectRatio: resolvedOptions.aspectRatio,
imageSize: resolvedOptions.imageSize,
};
}
export function createQuickEditPanelDraft(
sourceLayer: CanvasLayer,
options: {
imageModel?: string;
aspectRatio?: string;
imageSize?: string;
} = {},
): QuickEditPanelState {
const normalizedModel = normalizeEditorImageModel(
options.imageModel ?? sourceLayer.model,
);
const model = EDITOR_IMAGE_MODEL_OPTIONS.some(
(option) => option.value === normalizedModel,
)
? normalizedModel
: DEFAULT_IMAGE_MODEL;
const dimensionOptions =
EDITOR_IMAGE_DIMENSION_OPTIONS[
model as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
const supportedAspectRatios =
dimensionOptions.aspectRatios as readonly string[];
const supportedImageSizes = dimensionOptions.imageSizes as readonly string[];
const inferredAspectRatio = inferEditorImageAspectRatio(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
const inferredImageSize = inferEditorImageSizeLabel(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
const aspectRatio = supportedAspectRatios.includes(options.aspectRatio ?? '')
? options.aspectRatio
: supportedAspectRatios.includes(inferredAspectRatio)
? inferredAspectRatio
: (dimensionOptions.aspectRatios[0] ?? '1:1');
const imageSize = supportedImageSizes.includes(options.imageSize ?? '')
? options.imageSize
: supportedImageSizes.includes(inferredImageSize)
? inferredImageSize
: (dimensionOptions.imageSizes.find((size) => size === '1K') ??
dimensionOptions.imageSizes[0] ??
'1K');
return {
sourceLayerId: sourceLayer.id,
prompt: '',
size: formatImageSizeValue(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
),
aspectRatio,
imageSize,
model,
quickEditReferences: [],
status: 'idle',
};
}
export function createQuickEditGenerationDialogDraft({
sourceLayer,
prompt,
assetLabel,
status = 'idle',
references = [],
model = IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio,
imageSize,
frame,
}: {
sourceLayer: CanvasLayer;
prompt: string;
assetLabel?: string;
status?: CanvasGenerationDialogState['status'];
references?: NonNullable<QuickEditPanelState['quickEditReferences']>;
model?: string;
aspectRatio?: string;
imageSize?: string;
frame?: { width: number; height: number };
}): Omit<CanvasGenerationDialogState, 'id'> {
const resolvedOptions = createQuickEditPanelDraft(sourceLayer, {
imageModel: model,
aspectRatio,
imageSize,
});
const frameWidth = frame?.width ?? sourceLayer.width;
const frameHeight = frame?.height ?? sourceLayer.height;
return {
mode: 'quick-edit',
prompt,
assetLabel,
status,
composerOpen: false,
sourceLayerId: sourceLayer.id,
generationReferences: references.slice(0, 8),
imageModel: resolvedOptions.model,
aspectRatio: resolvedOptions.aspectRatio,
imageSize: resolvedOptions.imageSize,
placeholder: {
x: sourceLayer.x + sourceLayer.width + 32,
y: sourceLayer.y,
width: frameWidth,
height: frameHeight,
originalWidth: frameWidth,
originalHeight: frameHeight,
},
};
}
export function createLegacyImageRedrawGenerationDialogDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
}: {
sourceLayer: CanvasLayer;
canvasSize: CanvasSize;
viewport: CanvasViewport;
sourceDialog?: CanvasGenerationDialogState | null;
}): Omit<CanvasGenerationDialogState, 'id'> {
return placeDraftBesideSourceLayer(
restoreSharedImageOptions(
{
...createGenerateDialogDraft({ canvasSize, viewport }),
prompt: resolveUserGenerationPromptSnapshot(sourceLayer),
sourceLayerId: sourceLayer.id,
},
sourceLayer,
sourceDialog,
),
sourceLayer,
);
}
export function createCharacterAnimationPanelDraft(
layer: CanvasLayer,
): CharacterAnimationPanelState | null {
if (layer.assetKind !== 'character') {
return null;
}
return {
sourceLayerId: layer.id,
promptText: '',
resolution: '480p',
ratio: 'same',
frameCount: 32,
durationSeconds: 4,
status: 'idle',
};
}
export function createCharacterAnimationGenerationDialogDraft({
canvasSize,
viewport,
layer,
}: {
canvasSize: CanvasSize;
viewport: CanvasViewport;
layer: CanvasLayer;
}): Omit<CanvasGenerationDialogState, 'id'> | null {
if (layer.assetKind !== 'character') {
return null;
}
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
return {
mode: 'character-animation',
sourceLayerId: layer.id,
prompt: '',
status: 'idle',
composerOpen: true,
characterAnimationResolution: '480p',
characterAnimationRatio: 'same',
characterAnimationFrameCount: 32,
characterAnimationDurationSeconds: 4,
placeholder: {
x: worldCenter.x - CHARACTER_ANIMATION_FRAME_DISPLAY_SIZE.width / 2,
y: worldCenter.y - CHARACTER_ANIMATION_FRAME_DISPLAY_SIZE.height / 2,
width: CHARACTER_ANIMATION_FRAME_DISPLAY_SIZE.width,
height: CHARACTER_ANIMATION_FRAME_DISPLAY_SIZE.height,
originalWidth: CHARACTER_ANIMATION_FRAME_ORIGINAL_SIZE.width,
originalHeight: CHARACTER_ANIMATION_FRAME_ORIGINAL_SIZE.height,
},
};
}
export function assignCharacterSpecReference(
dialog: GenerateDialogState | null,
layer: CanvasLayer,
): GenerateDialogState | null {
return dialog?.mode === 'character' && isCharacterSpecReferenceLayer(layer)
? {
...resetFailedGenerationDialog(dialog),
characterSpecReference: createCanvasLayerReference(layer),
characterReferences: dialog.characterReferences,
composerOpen: true,
}
: dialog;
}
export function appendCharacterReference(
dialog: GenerateDialogState | null,
layer: CanvasLayer,
): GenerateDialogState | null {
return dialog?.mode === 'character' &&
getReferenceMediaType(layer) === 'image'
? {
...resetFailedGenerationDialog(dialog),
characterReferences: appendLimitedImageReferences(
dialog.characterReferences,
[createCanvasLayerReference(layer)],
resolveDialogExtraImageReferenceLimit(dialog),
),
composerOpen: true,
}
: dialog;
}
export function appendGenerationReference(
dialog: GenerateDialogState | null,
layer: CanvasLayer,
): GenerateDialogState | null {
if (dialog?.mode === 'spec') {
if (getReferenceMediaType(layer) !== 'image') {
return dialog;
}
const resetDialog = {
...resetFailedGenerationDialog(dialog),
composerOpen: true,
};
return setSpecGenerationReferences(
resetDialog,
dialog.specType === 'icon'
? [createCanvasLayerReference(layer)]
: appendLimitedImageReferences(
resolveSpecGenerationReferences(dialog),
[createCanvasLayerReference(layer)],
resolveDialogExtraImageReferenceLimit(dialog),
),
);
}
if (dialog?.mode === 'video' && !isSeedanceVideoModel(dialog.videoModel)) {
return dialog;
}
if (
dialog?.mode === 'generate' ||
dialog?.mode === 'scene' ||
dialog?.mode === 'quick-edit' ||
dialog?.mode === 'icon' ||
dialog?.mode === 'ui-design'
) {
if (getReferenceMediaType(layer) !== 'image') {
return dialog;
}
return {
...resetFailedGenerationDialog(dialog),
generationReferences: appendLimitedImageReferences(
dialog.generationReferences,
[createCanvasLayerReference(layer)],
resolveDialogExtraImageReferenceLimit(dialog),
),
composerOpen: true,
};
}
return dialog?.mode === 'video'
? {
...resetFailedGenerationDialog(dialog),
generationReferences: appendLimitedSeedanceCanvasReference(
dialog.generationReferences ?? [],
layer,
),
composerOpen: true,
}
: dialog;
}
/**
* 参考图弹窗确认:把「从项目素材中选择」进来的参考图整组替换,
* 画布点选与上传得到的参考图保持原样。
* 素材 id 通过 `sourceAssetId` 随引用进入提交数据,即本次任务的参考图 ID 快照。
*/
export function replaceProjectAssetPickerReferences(
dialog: GenerateDialogState | null,
references: CharacterReferenceImage[],
): GenerateDialogState | null {
if (
!dialog ||
(dialog.mode !== 'generate' &&
dialog.mode !== 'scene' &&
dialog.mode !== 'quick-edit' &&
dialog.mode !== 'icon' &&
dialog.mode !== 'ui-design')
) {
return dialog;
}
return {
...resetFailedGenerationDialog(dialog),
generationReferences: appendLimitedImageReferences(
(dialog.generationReferences ?? []).filter(
(reference) => !isProjectAssetPickerReference(reference),
),
references,
resolveDialogExtraImageReferenceLimit(dialog),
),
composerOpen: true,
};
}
export function appendPublicationReference(
dialog: GenerateDialogState | null,
layer: CanvasLayer,
): GenerateDialogState | null {
return dialog?.mode === 'publication' &&
getReferenceMediaType(layer) === 'image'
? {
...resetFailedGenerationDialog(dialog),
publicationReferences: appendLimitedImageReferences(
dialog.publicationReferences,
[createCanvasLayerReference(layer)],
resolveDialogExtraImageReferenceLimit(dialog),
),
composerOpen: true,
}
: dialog;
}
export function assignIconSpecReference(
dialog: GenerateDialogState | null,
layer: CanvasLayer,
): GenerateDialogState | null {
if (!isIconSpecReferenceLayer(layer)) {
return dialog;
}
return dialog?.mode === 'icon'
? {
...resetFailedGenerationDialog(dialog),
iconSpecReference: createCanvasLayerReference(layer),
generationReferences: appendLimitedImageReferences(
[],
dialog.generationReferences ?? [],
resolveDialogExtraImageReferenceLimit(dialog),
),
composerOpen: true,
}
: dialog;
}
export function assignUiDesignSpecReference(
dialog: GenerateDialogState | null,
layer: CanvasLayer,
): GenerateDialogState | null {
if (!isIconSpecReferenceLayer(layer)) {
return dialog;
}
return dialog?.mode === 'ui-design'
? {
...resetFailedGenerationDialog(dialog),
uiDesignSpecReference: createCanvasLayerReference(layer),
generationReferences: dialog.generationReferences,
composerOpen: true,
}
: dialog;
}
export function updateSpecFormDialogValue(
dialog: GenerateDialogState | null,
key: keyof SpecFormValues,
value: string,
): GenerateDialogState | null {
if (dialog?.mode !== 'spec') {
return dialog;
}
const specType = dialog.specType ?? 'custom';
return {
...resetFailedGenerationDialog(dialog),
specValues: {
...DEFAULT_SPEC_FORM_VALUES[specType],
...dialog.specValues,
[key]: value,
},
};
}
export function updateIconDescriptionsTextInDialog(
dialog: GenerateDialogState | null,
value: string,
): GenerateDialogState | null {
return dialog?.mode === 'icon'
? {
...resetFailedGenerationDialog(dialog),
prompt: value,
iconDescriptions: [],
}
: dialog;
}
export function appendQuickEditReference(
panel: QuickEditPanelState | null,
layer: CanvasLayer,
): QuickEditPanelState | null {
if (!panel || getReferenceMediaType(layer) !== 'image') {
return panel;
}
return {
...resetFailedGenerationPanel(panel),
quickEditReferences: appendLimitedQuickEditReferences(
panel.quickEditReferences,
[createCanvasLayerReference(layer)],
),
};
}
export function updateCharacterAnimationDurationPanel(
panel: CharacterAnimationPanelState | null,
frameCountValue: string,
): CharacterAnimationPanelState | null {
const option = CHARACTER_ANIMATION_DURATION_OPTIONS.find(
(item) => String(item.frameCount) === frameCountValue,
);
if (!option || !panel) {
return panel;
}
return {
...panel,
frameCount: option.frameCount,
durationSeconds: option.durationSeconds,
status: panel.status === 'failed' ? 'idle' : panel.status,
errorMessage: panel.status === 'failed' ? undefined : panel.errorMessage,
};
}
export function hideGeneratedLayerComposerAfterBlur(
dialog: GenerateDialogState | null,
): GenerateDialogState | null {
return (dialog?.mode === 'generate' ||
dialog?.mode === 'scene' ||
dialog?.mode === 'spec' ||
dialog?.mode === 'character' ||
dialog?.mode === 'icon' ||
dialog?.mode === 'ui-design' ||
dialog?.mode === 'quick-edit' ||
dialog?.mode === 'character-animation' ||
dialog?.mode === 'video' ||
dialog?.mode === 'publication' ||
dialog?.mode === 'audio-sound-effect' ||
dialog?.mode === 'audio-background-music') &&
dialog.status !== 'generating'
? {
...dialog,
composerOpen: false,
}
: dialog;
}
export function closeGenerateComposerDialog(
dialog: GenerateDialogState | null,
): GenerateDialogState | null {
return dialog?.mode === 'generate' ||
dialog?.mode === 'scene' ||
dialog?.mode === 'spec' ||
dialog?.mode === 'character' ||
dialog?.mode === 'icon' ||
dialog?.mode === 'ui-design' ||
dialog?.mode === 'quick-edit' ||
dialog?.mode === 'character-animation' ||
dialog?.mode === 'video' ||
dialog?.mode === 'publication' ||
dialog?.mode === 'audio-sound-effect' ||
dialog?.mode === 'audio-background-music'
? {
...dialog,
composerOpen: false,
}
: dialog;
}