Files
Genarrative/src/components/image-editor/ImageCanvasGenerationDialogModel.ts
T
k88936 e564db43f1 合并主分支
解决合并冲突
2026-08-08 10:48:13 +08:00

2227 lines
65 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 {
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<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 === 'ui';
}
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,
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<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) {
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']> {
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<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),
soundDurationSeconds: resolveAudioRedrawSoundDuration(sourceLayer),
};
}
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 === '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<CanvasGenerationDialogState, 'id'>;
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<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 'ui';
}
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,
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<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 {
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<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 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<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;
}
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;
}