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