e564db43f1
解决合并冲突
2227 lines
65 KiB
TypeScript
2227 lines
65 KiB
TypeScript
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;
|
||
}
|