import { DEFAULT_EDITOR_SCENE_STYLE_PRESET, resolveEditorSceneStylePresetByLabel, } from '../../../packages/shared/src/contracts/editorScene'; import { formatImageSizeValue } from './ImageCanvasEditorModel'; import type { CanvasGenerationDialogState, CanvasGenerationInputReference, CanvasLayer, CanvasViewport, CharacterAnimationPanelState, 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'; 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, }; } 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, 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(); 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 { 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 { 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 === 'ui'; } export function createSpecDialogDraft({ canvasSize, viewport, specType, }: { canvasSize: CanvasSize; viewport: CanvasViewport; specType: SpecGenerationType; }): Omit { 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 { 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 { 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 { 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 { 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 { const worldCenter = getViewportWorldCenter({ canvasSize, viewport }); return { mode: 'audio-sound-effect', prompt: '', status: 'idle', composerOpen: true, soundModel: DEFAULT_SOUND_EFFECT_MODEL, soundDurationSeconds: DEFAULT_SOUND_EFFECT_DURATION_SECONDS, 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 { 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) { 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 { const fieldValue = findGenerationInputFieldValue(sourceLayer, [ 'model', ])?.trim(); // 中文注释:编辑器音效改造入口当前只暴露 Vidu audio1.0,历史快照里的其他模型统一回落到默认模型。 return fieldValue === DEFAULT_SOUND_EFFECT_MODEL ? 'audio1.0' : 'audio1.0'; } function resolveAudioRedrawSoundDuration(sourceLayer: CanvasLayer) { const fieldValue = findGenerationInputFieldValue(sourceLayer, [ '时长', 'duration', ]); const matchedValue = fieldValue?.match(/\d+/u)?.[0]; const duration = matchedValue ? Number.parseInt(matchedValue, 10) : null; if (!duration || !Number.isFinite(duration)) { return DEFAULT_SOUND_EFFECT_DURATION_SECONDS; } return Math.min(10, Math.max(2, duration)); } export function createAudioRedrawGenerationDialogDraft( sourceLayer: CanvasLayer, ): Omit | 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), soundDurationSeconds: resolveAudioRedrawSoundDuration(sourceLayer), }; } export function createVideoRedrawGenerationDialogDraft( sourceLayer: CanvasLayer, ): Omit | 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): 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, >(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 === 'spec.generate') { const specTypeValue = getNormalizedString(sourceLayer, 'specType', 'custom'); const specType: SpecGenerationType = specTypeValue === 'character' || specTypeValue === 'ui' || 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'), }, specReference: resolveNormalizedReferences(sourceLayer, 'reference', availableLayers)[0] ?? null, }, 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; return placeDraftBesideSourceLayer( videoDraft, sourceLayer, ); } if ( inputs.action === 'audio.sound-effect.generate' || inputs.action === 'audio.background-music.generate' ) { 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' ? getNormalizedNumber( sourceLayer, 'durationSeconds', DEFAULT_SOUND_EFFECT_DURATION_SECONDS, ) : 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; } if (inputs.action === 'image.edit') { const source = resolveNormalizedReferences(sourceLayer, 'source', availableLayers)[0]; const sourceLayerForEdit = source ? (availableLayers ?? []).find( (layer) => createCanvasLayerReference(layer).id === source.id, ) : undefined; if (!sourceLayerForEdit) { return null; } const size = resolveEditorImageGenerationPixelSize({ model: getNormalizedString(sourceLayer, 'model', DEFAULT_IMAGE_MODEL), aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '1:1'), imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'), }); return { ...createQuickEditGenerationDialogDraft({ sourceLayer: sourceLayerForEdit, prompt, references, model: getNormalizedString(sourceLayer, 'model', DEFAULT_IMAGE_MODEL), aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '1:1'), imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'), frame: size, }), composerOpen: true, isRedrawAsNewArtifact: true, }; } return null; } function restoreSharedImageOptions( draft: Omit, sourceLayer: CanvasLayer, sourceDialog?: CanvasGenerationDialogState | null, ): Omit { 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 'ui'; } function restoreSpecValuesFromLayer( draft: Omit, sourceLayer: CanvasLayer, specType: SpecGenerationType, sourceDialog?: CanvasGenerationDialogState | null, ): Omit { 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, sourceLayer: CanvasLayer, sourceDialog?: CanvasGenerationDialogState | null, ): Omit { 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, sourceLayer: CanvasLayer, sourceDialog?: CanvasGenerationDialogState | null, ): Omit { 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, soundDurationSeconds: sourceDialog?.soundDurationSeconds ?? draft.soundDurationSeconds, 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); return placeDraftBesideSourceLayer( restoreSpecValuesFromLayer( { ...createSpecDialogDraft({ canvasSize, viewport, specType }), prompt, sourceLayerId: sourceLayer.id, specReference: sourceDialog?.mode === 'spec' ? sourceDialog.specReference : null, }, sourceLayer, specType, sourceDialog, ), sourceLayer, ); } return null; } export function createUiDesignGenerationDialogDraft({ canvasSize, viewport, imageModel, }: { canvasSize: CanvasSize; viewport: CanvasViewport; imageModel: string; }): Omit { 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 { mode: 'quick-edit', 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; model?: string; aspectRatio?: string; imageSize?: string; frame?: { width: number; height: number }; }): Omit { 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 createRedrawPanelDraft( sourceLayer: CanvasLayer, options: { imageModel?: string; aspectRatio?: string; imageSize?: string; } = {}, ): QuickEditPanelState { return { ...createQuickEditPanelDraft(sourceLayer, options), mode: 'redraw', model: normalizeEditorImageModel(options.imageModel ?? sourceLayer.model), prompt: resolveUserGenerationPromptSnapshot(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 | 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; } return { ...resetFailedGenerationDialog(dialog), specReference: createCanvasLayerReference(layer), composerOpen: true, }; } 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; } 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 || panel.mode === 'redraw' || 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; }