修正画布改造工具恢复链路 #158
File diff suppressed because one or more lines are too long
@@ -130,7 +130,10 @@ export type CanvasLayer = {
|
||||
id: string;
|
||||
resourceId: string;
|
||||
resourcePersistenceState?:
|
||||
'pending' | 'registered' | 'self-contained-local' | 'unresolved-local';
|
||||
| 'pending'
|
||||
| 'registered'
|
||||
| 'self-contained-local'
|
||||
| 'unresolved-local';
|
||||
title: string;
|
||||
src: string;
|
||||
mediaType?: CanvasMediaType;
|
||||
@@ -263,7 +266,6 @@ export type GenerateDialogState = {
|
||||
assetLabel?: string;
|
||||
status: 'idle' | 'generating' | 'pending-confirmation' | 'failed';
|
||||
composerOpen?: boolean;
|
||||
isRedrawAsNewArtifact?: boolean;
|
||||
sourceLayerId?: string;
|
||||
generatedLayerId?: string;
|
||||
specType?: SpecGenerationType;
|
||||
@@ -442,7 +444,6 @@ export type CanvasContextMenuState =
|
||||
|
||||
export type QuickEditPanelState = {
|
||||
referenceUploadContextId?: string;
|
||||
mode?: 'quick-edit' | 'redraw';
|
||||
sourceLayerId: string;
|
||||
prompt: string;
|
||||
assetLabel?: string;
|
||||
@@ -469,7 +470,12 @@ export type CropExpandPanelState = {
|
||||
};
|
||||
|
||||
export type CropExpandRatioValue =
|
||||
'free' | '1:1' | '4:3' | '3:4' | '16:9' | '9:16';
|
||||
| 'free'
|
||||
| '1:1'
|
||||
| '4:3'
|
||||
| '3:4'
|
||||
| '16:9'
|
||||
| '9:16';
|
||||
|
||||
export type CropExpandResizeHandle =
|
||||
| 'left'
|
||||
|
||||
@@ -1169,8 +1169,9 @@ export function ImageCanvasEditorView({
|
||||
imageSequenceDurationMs: asset.imageSequenceDurationMs ?? undefined,
|
||||
assetKind: canvasAssetKindOrNull(asset.assetKind),
|
||||
generationInputs: generationInputsOrNull(asset.generationInputs),
|
||||
generationInputsHydrationState:
|
||||
hydrateCanvasGenerationInputsResult(asset.generationInputs).state,
|
||||
generationInputsHydrationState: hydrateCanvasGenerationInputsResult(
|
||||
asset.generationInputs,
|
||||
).state,
|
||||
},
|
||||
]);
|
||||
setAssetFolders((currentFolders) =>
|
||||
@@ -1750,7 +1751,7 @@ export function ImageCanvasEditorView({
|
||||
: null;
|
||||
}
|
||||
if (target === 'quick-edit-reference') {
|
||||
return quickEditPanel && quickEditPanel.mode !== 'redraw'
|
||||
return quickEditPanel
|
||||
? {
|
||||
label: '快速编辑参考图',
|
||||
currentCount: imageReferenceCount(
|
||||
|
||||
@@ -21,10 +21,10 @@ import {
|
||||
createGenerateDialogDraft,
|
||||
createIconGenerationDialogDraft,
|
||||
createLayerGenerationDialogDraft,
|
||||
createLegacyImageRedrawGenerationDialogDraft,
|
||||
createPublicationGenerationDialogDraft,
|
||||
createQuickEditGenerationDialogDraft,
|
||||
createQuickEditPanelDraft,
|
||||
createRedrawPanelDraft,
|
||||
createSameSourceGenerationDialogDraft,
|
||||
createSceneGenerationDialogDraft,
|
||||
createSoundEffectGenerationDialogDraft,
|
||||
@@ -469,7 +469,6 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
imageSize: '1K',
|
||||
});
|
||||
expect(createQuickEditPanelDraft(sourceLayer)).toEqual({
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '',
|
||||
size: '1024x768',
|
||||
@@ -479,7 +478,13 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
quickEditReferences: [],
|
||||
status: 'idle',
|
||||
});
|
||||
expect(createRedrawPanelDraft(sourceLayer).prompt).toBe('原图提示');
|
||||
expect(
|
||||
createLegacyImageRedrawGenerationDialogDraft({
|
||||
sourceLayer,
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
}),
|
||||
).toMatchObject({ mode: 'generate', prompt: '原图提示' });
|
||||
expect(createCharacterAnimationPanelDraft(sourceLayer)).toEqual({
|
||||
sourceLayerId: 'layer-source',
|
||||
promptText: '',
|
||||
@@ -590,21 +595,23 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
|
||||
for (const testCase of cases) {
|
||||
expect(
|
||||
createRedrawPanelDraft(
|
||||
createLayer({
|
||||
createLegacyImageRedrawGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
prompt: '【系统内置】完整拼接提示词,包含默认规则和模型约束。',
|
||||
generationInputs: {
|
||||
fields: testCase.fields,
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
).prompt,
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
}).prompt,
|
||||
).toBe(testCase.expectedPrompt);
|
||||
}
|
||||
|
||||
expect(
|
||||
createRedrawPanelDraft(
|
||||
createLayer({
|
||||
createLegacyImageRedrawGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
prompt: '【系统内置】固定素材提取提示词',
|
||||
generationInputs: {
|
||||
fields: [
|
||||
@@ -618,7 +625,9 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
).prompt,
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
}).prompt,
|
||||
).toBe('');
|
||||
});
|
||||
|
||||
@@ -1269,7 +1278,7 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('restores quick-edit references and the persisted image model', () => {
|
||||
it('does not restore in-place image edits as remodel generators', () => {
|
||||
const editSource = createLayer({
|
||||
id: 'layer-edit-source',
|
||||
resourceId: 'resource-edit-source',
|
||||
@@ -1279,55 +1288,44 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
resourceId: 'resource-style-reference',
|
||||
title: '风格参考',
|
||||
});
|
||||
const editResult = createLayer({
|
||||
id: 'layer-edit-result',
|
||||
resourceId: 'resource-edit-result',
|
||||
sourceType: 'generated',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'image.edit',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '提示词', value: '改成雨天' },
|
||||
{ id: 'model', title: '模型', value: IMAGE_MODEL_NANOBANANA2 },
|
||||
{ id: 'aspectRatio', title: '比例', value: '16:9' },
|
||||
{ id: 'imageSize', title: '尺寸', value: '2K' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
id: 'source',
|
||||
title: '原图',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-edit-source',
|
||||
},
|
||||
{
|
||||
id: 'reference',
|
||||
title: '参考图 1',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-style-reference',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
createLayerGenerationDialogDraft({
|
||||
sourceLayer: createLayer({
|
||||
id: 'layer-edit-result',
|
||||
resourceId: 'resource-edit-result',
|
||||
sourceType: 'generated',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'image.edit',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '提示词', value: '改成雨天' },
|
||||
{ id: 'model', title: '模型', value: IMAGE_MODEL_NANOBANANA2 },
|
||||
{ id: 'aspectRatio', title: '比例', value: '16:9' },
|
||||
{ id: 'imageSize', title: '尺寸', value: '2K' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
id: 'source',
|
||||
title: '原图',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-edit-source',
|
||||
},
|
||||
{
|
||||
id: 'reference',
|
||||
title: '参考图 1',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-style-reference',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
sourceLayer: editResult,
|
||||
availableLayers: [editSource, styleReference],
|
||||
canvasSize: { width: 960, height: 720 },
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: 'quick-edit',
|
||||
prompt: '改成雨天',
|
||||
imageModel: IMAGE_MODEL_NANOBANANA2,
|
||||
aspectRatio: '16:9',
|
||||
imageSize: '2K',
|
||||
generationReferences: [
|
||||
expect.objectContaining({
|
||||
id: 'canvas-layer-style-reference',
|
||||
label: '风格参考',
|
||||
}),
|
||||
],
|
||||
});
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('uses the V2 source reference ahead of a conflicting legacy animation source', () => {
|
||||
|
||||
@@ -1315,40 +1315,6 @@ function createNormalizedGenerationDialogDraft({
|
||||
: 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;
|
||||
}
|
||||
|
||||
@@ -1956,7 +1922,6 @@ export function createQuickEditPanelDraft(
|
||||
dimensionOptions.imageSizes[0] ??
|
||||
'1K');
|
||||
return {
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: sourceLayer.id,
|
||||
prompt: '',
|
||||
size: formatImageSizeValue(
|
||||
@@ -2021,20 +1986,29 @@ export function createQuickEditGenerationDialogDraft({
|
||||
};
|
||||
}
|
||||
|
||||
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 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(
|
||||
@@ -2261,11 +2235,7 @@ export function appendQuickEditReference(
|
||||
panel: QuickEditPanelState | null,
|
||||
layer: CanvasLayer,
|
||||
): QuickEditPanelState | null {
|
||||
if (
|
||||
!panel ||
|
||||
panel.mode === 'redraw' ||
|
||||
getReferenceMediaType(layer) !== 'image'
|
||||
) {
|
||||
if (!panel || getReferenceMediaType(layer) !== 'image') {
|
||||
return panel;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
createCharacterAnimationResultLayer,
|
||||
createGeneratedResultLayer,
|
||||
createIconSpritesheetResultLayers,
|
||||
createQuickEditResultLayer,
|
||||
createVideoResultLayer,
|
||||
} from './ImageCanvasGenerationLayerModel';
|
||||
|
||||
@@ -232,57 +231,6 @@ describe('ImageCanvasGenerationLayerModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('creates quick edit layers centered on the generating placeholder', () => {
|
||||
const sourceLayer = createSourceLayer();
|
||||
const layer = createQuickEditResultLayer({
|
||||
generated: createGenerated({ width: 1536, height: 1024 }),
|
||||
generatedIndex: 10,
|
||||
sourceLayer,
|
||||
generationInputs: createGenerationInputs(),
|
||||
frame: {
|
||||
x: 600,
|
||||
y: 300,
|
||||
width: 320,
|
||||
height: 240,
|
||||
originalWidth: 1536,
|
||||
originalHeight: 1024,
|
||||
},
|
||||
});
|
||||
|
||||
expect(layer).toMatchObject({
|
||||
id: 'layer-quick-edit-10',
|
||||
x: -8,
|
||||
y: -92,
|
||||
width: 1536,
|
||||
height: 1024,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates quick edit layers with source group and asset kind preserved', () => {
|
||||
const sourceLayer = createSourceLayer();
|
||||
const layer = createQuickEditResultLayer({
|
||||
generated: createGenerated({ width: 1536, height: 1024 }),
|
||||
generatedIndex: 9,
|
||||
sourceLayer,
|
||||
generationInputs: createGenerationInputs(),
|
||||
});
|
||||
|
||||
expect(layer).toMatchObject({
|
||||
id: 'layer-quick-edit-9',
|
||||
resourceId: 'local-resource-quick-edit-9',
|
||||
title: '源图 快速编辑',
|
||||
x: 472,
|
||||
y: 140,
|
||||
width: 1536,
|
||||
height: 1024,
|
||||
originalWidth: 1536,
|
||||
originalHeight: 1024,
|
||||
sourceResourceId: 'resource-source',
|
||||
groupId: 'group-a',
|
||||
assetKind: 'spec',
|
||||
});
|
||||
});
|
||||
|
||||
it('applies quick edits to the source layer in place', () => {
|
||||
const sourceLayer = createSourceLayer({
|
||||
width: 320,
|
||||
|
||||
@@ -32,16 +32,6 @@ type GeneratedResultLayerOptions = {
|
||||
generationInputs?: CanvasGenerationInputs;
|
||||
};
|
||||
|
||||
type QuickEditResultLayerOptions = {
|
||||
generated: EditorImageGenerationResult;
|
||||
generatedIndex: number;
|
||||
sourceLayer: CanvasLayer;
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
mode?: 'quick-edit' | 'redraw';
|
||||
frame?: GenerateDialogState['placeholder'];
|
||||
title?: string;
|
||||
};
|
||||
|
||||
type IconSpritesheetResultLayerOptions = {
|
||||
generated: EditorIconSpritesheetGenerationResult;
|
||||
iconResults?: EditorIconSpritesheetIconResult[] | null;
|
||||
@@ -183,67 +173,6 @@ export function createGeneratedResultLayer({
|
||||
);
|
||||
}
|
||||
|
||||
export function createQuickEditResultLayer({
|
||||
generated,
|
||||
generatedIndex,
|
||||
sourceLayer,
|
||||
generationInputs,
|
||||
mode = 'quick-edit',
|
||||
frame,
|
||||
title,
|
||||
}: QuickEditResultLayerOptions): CanvasLayer {
|
||||
const originalWidth =
|
||||
frame?.originalWidth ||
|
||||
generated.width ||
|
||||
sourceLayer.originalWidth ||
|
||||
1024;
|
||||
const originalHeight =
|
||||
frame?.originalHeight ||
|
||||
generated.height ||
|
||||
sourceLayer.originalHeight ||
|
||||
1024;
|
||||
const { width, height } = resolveLayerResolutionSize(
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
{
|
||||
width: sourceLayer.width,
|
||||
height: sourceLayer.height,
|
||||
},
|
||||
);
|
||||
const frameX =
|
||||
frame && frame.width > 0
|
||||
? frame.x + frame.width / 2 - width / 2
|
||||
: undefined;
|
||||
const frameY =
|
||||
frame && frame.height > 0
|
||||
? frame.y + frame.height / 2 - height / 2
|
||||
: undefined;
|
||||
|
||||
return applyGeneratedMetadata(
|
||||
{
|
||||
id: `layer-quick-edit-${generatedIndex}`,
|
||||
resourceId: `local-resource-quick-edit-${generatedIndex}`,
|
||||
title:
|
||||
title ??
|
||||
`${sourceLayer.title} ${mode === 'redraw' ? '重绘' : '快速编辑'}`,
|
||||
src: generated.imageSrc,
|
||||
x: frameX ?? sourceLayer.x + sourceLayer.width + 32,
|
||||
y: frameY ?? sourceLayer.y,
|
||||
width,
|
||||
height,
|
||||
originalWidth,
|
||||
originalHeight,
|
||||
zIndex: generatedIndex + 10,
|
||||
sourceType: generated.sourceType,
|
||||
sourceResourceId: sourceLayer.resourceId,
|
||||
groupId: sourceLayer.groupId,
|
||||
assetKind: sourceLayer.assetKind,
|
||||
},
|
||||
generated,
|
||||
generationInputs,
|
||||
);
|
||||
}
|
||||
|
||||
export function applyImageEditResultToSourceLayer({
|
||||
generated,
|
||||
sourceLayer,
|
||||
|
||||
@@ -100,10 +100,7 @@ describe('ImageCanvasQuickEditPanelView', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
setPlainTextEditorValue(
|
||||
screen.getByLabelText('快速编辑提示词'),
|
||||
'新提示',
|
||||
);
|
||||
setPlainTextEditorValue(screen.getByLabelText('快速编辑提示词'), '新提示');
|
||||
|
||||
expect(screen.getByLabelText('当前提示词').textContent).toBe('新提示');
|
||||
expect(screen.getByLabelText('当前尺寸').textContent).toBe('1024x1024');
|
||||
@@ -141,9 +138,7 @@ describe('ImageCanvasQuickEditPanelView', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('alert').textContent).toContain(
|
||||
'图集快速编辑失败',
|
||||
);
|
||||
expect(screen.getByRole('alert').textContent).toContain('图集快速编辑失败');
|
||||
});
|
||||
|
||||
it('submits without rendering a standalone close button', () => {
|
||||
@@ -171,27 +166,6 @@ describe('ImageCanvasQuickEditPanelView', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('shows configured mud point price inside redraw submit button', () => {
|
||||
render(
|
||||
<QuickEditPanelHarness
|
||||
initialPanel={{
|
||||
sourceLayerId: 'layer-a',
|
||||
prompt: '重绘提示',
|
||||
size: '1024x1024',
|
||||
aspectRatio: '1:1',
|
||||
imageSize: '1K',
|
||||
model: 'gpt-image-2',
|
||||
status: 'idle',
|
||||
mode: 'redraw',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: '生成' }).textContent).toBe(
|
||||
'生成3泥点',
|
||||
);
|
||||
});
|
||||
|
||||
it('hides quick edit references from the simplified panel', () => {
|
||||
render(
|
||||
<QuickEditPanelHarness
|
||||
|
||||
@@ -51,9 +51,8 @@ function resetFailedPanelStatus<
|
||||
function createQuickEditDialog(
|
||||
panel: QuickEditPanelState,
|
||||
): GenerateDialogState {
|
||||
const isQuickEdit = (panel.mode ?? 'quick-edit') === 'quick-edit';
|
||||
return {
|
||||
mode: isQuickEdit ? 'quick-edit' : 'generate',
|
||||
mode: 'quick-edit',
|
||||
prompt: panel.prompt,
|
||||
status: panel.status,
|
||||
errorMessage: panel.errorMessage,
|
||||
@@ -107,17 +106,11 @@ export function ImageCanvasQuickEditPanelView({
|
||||
hasPendingImageReferenceUploads = false,
|
||||
onSubmit,
|
||||
}: ImageCanvasQuickEditPanelViewProps) {
|
||||
const isRedraw = panel.mode === 'redraw';
|
||||
const isImageQuickEdit = !isRedraw && sourceLayer.mediaType !== 'video';
|
||||
const isImageQuickEdit = sourceLayer.mediaType !== 'video';
|
||||
const referenceCount = panel.quickEditReferences?.length ?? 0;
|
||||
const canAddReferences =
|
||||
!isRedraw &&
|
||||
referenceCount <
|
||||
resolveExtraImageReferenceLimit(
|
||||
panel.model,
|
||||
QUICK_EDIT_REFERENCE_LIMIT,
|
||||
1,
|
||||
);
|
||||
resolveExtraImageReferenceLimit(panel.model, QUICK_EDIT_REFERENCE_LIMIT, 1);
|
||||
const quickEditDialog = createQuickEditDialog(panel);
|
||||
|
||||
return (
|
||||
@@ -154,21 +147,19 @@ export function ImageCanvasQuickEditPanelView({
|
||||
onRememberImageModel={onRememberImageModel}
|
||||
hasPendingImageReferenceUploads={hasPendingImageReferenceUploads}
|
||||
onSubmit={onSubmit}
|
||||
dialogLabel={isRedraw ? '重绘图片' : '快速编辑图片'}
|
||||
includeDimensions={isRedraw || isImageQuickEdit}
|
||||
dialogLabel="快速编辑图片"
|
||||
includeDimensions={isImageQuickEdit}
|
||||
includeModel={true}
|
||||
includeReferences={false}
|
||||
promptLabel={isRedraw ? '重绘提示词' : '快速编辑提示词'}
|
||||
promptPlaceholder={
|
||||
isRedraw ? '把画面重绘成什么样?' : '你希望素材如何修改?'
|
||||
}
|
||||
optionLabelPrefix={isRedraw ? '重绘' : '快速编辑'}
|
||||
promptLabel="快速编辑提示词"
|
||||
promptPlaceholder="你希望素材如何修改?"
|
||||
optionLabelPrefix="快速编辑"
|
||||
dimensionRatioAriaLabelPrefix="比例"
|
||||
dimensionSizeAriaLabelPrefix="尺寸"
|
||||
referenceUploadTarget="quick-edit-reference"
|
||||
referenceButtonLabel={isRedraw ? '' : '添加快速编辑参考图'}
|
||||
referenceButtonAriaLabel={isRedraw ? undefined : '添加快速编辑参考图'}
|
||||
referenceMenuLabel={isRedraw ? '' : '快速编辑参考图来源'}
|
||||
referenceButtonLabel="添加快速编辑参考图"
|
||||
referenceButtonAriaLabel="添加快速编辑参考图"
|
||||
referenceMenuLabel="快速编辑参考图来源"
|
||||
referenceSlotTone="quick-edit"
|
||||
referenceSlotDisabled={!canAddReferences}
|
||||
showReferenceAddButton={false}
|
||||
@@ -180,9 +171,9 @@ export function ImageCanvasQuickEditPanelView({
|
||||
promptClassName="image-canvas-editor__generation-prompt image-canvas-editor__quick-edit-prompt"
|
||||
referenceSlotClassName="image-canvas-editor__quick-edit-reference"
|
||||
submitButtonClassName="image-canvas-editor__generation-submit image-canvas-editor__quick-edit-submit"
|
||||
submitLabel={isRedraw ? '生成' : '修改'}
|
||||
submitAriaLabel={isRedraw ? '生成' : '修改'}
|
||||
submittingStatusLabel={isRedraw ? '生成中' : '修改中'}
|
||||
submitLabel="修改"
|
||||
submitAriaLabel="修改"
|
||||
submittingStatusLabel="修改中"
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
/>
|
||||
|
||||
@@ -98,9 +98,7 @@ describe('ImageCanvasSelectedLayerToolbarView', () => {
|
||||
);
|
||||
expect(props.onRemoveBackground).toHaveBeenCalledWith(props.selectedLayer);
|
||||
expect(props.onPerfectPixel).toHaveBeenCalledWith(props.selectedLayer);
|
||||
expect(props.onOpenRedrawPanel).toHaveBeenCalledWith(
|
||||
props.selectedLayer,
|
||||
);
|
||||
expect(props.onOpenRedrawPanel).toHaveBeenCalledWith(props.selectedLayer);
|
||||
expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer);
|
||||
expect(
|
||||
within(toolbar).queryByRole('button', { name: '删除图片' }),
|
||||
@@ -391,7 +389,7 @@ describe('ImageCanvasSelectedLayerToolbarView', () => {
|
||||
expect(screen.queryByRole('button', { name: '改造' })).toBeNull();
|
||||
});
|
||||
|
||||
it('hides redraw for in-place quick-edit results', () => {
|
||||
it('keeps quick edit but hides redraw for in-place quick-edit results', () => {
|
||||
renderSelectedToolbar({
|
||||
selectedLayer: createLayer({
|
||||
generationInputs: {
|
||||
|
||||
@@ -285,7 +285,7 @@ export function applyQuickEditReferenceUpload({
|
||||
panel: QuickEditPanelState | null;
|
||||
references: CharacterReferenceImage[];
|
||||
}): QuickEditPanelState | null {
|
||||
if (!panel || panel.mode === 'redraw') {
|
||||
if (!panel) {
|
||||
return panel;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -901,7 +901,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialQuickEditPanel={{
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '改成雨天',
|
||||
assetLabel: ' 雨天勇者 ',
|
||||
@@ -939,7 +938,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialQuickEditPanel={{
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '改成雨天',
|
||||
size: '2048x1152',
|
||||
@@ -965,7 +963,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
<SubmissionWorkflowHarness
|
||||
initialLayers={[createLayer({ assetKind: 'icon' })]}
|
||||
initialQuickEditPanel={{
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '修改图标',
|
||||
size: '1024x1024',
|
||||
@@ -993,7 +990,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
<SubmissionWorkflowHarness
|
||||
initialLayers={[createLayer({ assetKind: 'icon-spritesheet' })]}
|
||||
initialQuickEditPanel={{
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '调整图集布局',
|
||||
size: '1024x1024',
|
||||
@@ -1218,7 +1214,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
initialQuickEditPanel={{
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '对1号红色圈选框里的内容做以下修改:改成蓝色',
|
||||
size: '1024x768',
|
||||
@@ -1411,70 +1406,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
expect(refreshWalletBalance).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('submits character redraws through the character image prompt contract', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({
|
||||
prompt: '角色换成蓝色披风',
|
||||
warning: {
|
||||
code: 'postprocess-failed-source-preserved',
|
||||
reason: '生成任务成功,后处理失败。',
|
||||
},
|
||||
}),
|
||||
);
|
||||
render(
|
||||
<SubmissionWorkflowHarness
|
||||
projectId="editor-project-1"
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
assetKind: 'character',
|
||||
prompt: '原始角色设定',
|
||||
}),
|
||||
]}
|
||||
initialQuickEditPanel={{
|
||||
mode: 'redraw',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: ' 角色换成蓝色披风 ',
|
||||
assetLabel: ' 蓝披风勇者 ',
|
||||
size: '1024x768',
|
||||
model: 'gpt-image-2',
|
||||
status: 'idle',
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '角色换成蓝色披风',
|
||||
size: '1024x768',
|
||||
aspectRatio: '4:3',
|
||||
imageSize: '1K',
|
||||
kind: 'character',
|
||||
model: 'gpt-image-2',
|
||||
referenceImageSrcs: ['resource-source'],
|
||||
assetKind: 'character',
|
||||
assetLabel: '蓝披风勇者',
|
||||
canvasCompletion: expect.objectContaining({
|
||||
title: '蓝披风勇者',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-quick-edit-1:蓝披风勇者:resource-source:character',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe(
|
||||
'layer-source:idle:角色换成蓝色披风:-',
|
||||
);
|
||||
expect(screen.getByTestId('generation-warning').textContent).toBe(
|
||||
'生成任务成功,后处理失败。',
|
||||
);
|
||||
});
|
||||
|
||||
it('submits video quick edits through the video API with the source video', async () => {
|
||||
const applyProjectSnapshot = vi.fn();
|
||||
const upsertGeneratedAsset = vi.fn();
|
||||
@@ -1523,7 +1454,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
}),
|
||||
]}
|
||||
initialQuickEditPanel={{
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '把当前图改成雨天',
|
||||
size: '854x480',
|
||||
@@ -1683,7 +1613,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
|
||||
}),
|
||||
]}
|
||||
initialQuickEditPanel={{
|
||||
mode: 'quick-edit',
|
||||
sourceLayerId: 'layer-source',
|
||||
prompt: '让当前图跳得更高',
|
||||
size: '1024x1024',
|
||||
|
||||
@@ -57,12 +57,10 @@ import {
|
||||
createCharacterAnimationResultLayer,
|
||||
createGeneratedResultLayer,
|
||||
createIconSpritesheetResultLayers,
|
||||
createQuickEditResultLayer,
|
||||
createVideoResultLayer,
|
||||
} from './ImageCanvasGenerationLayerModel';
|
||||
import {
|
||||
buildCharacterAnimationGenerationInputs,
|
||||
buildEditGenerationInputs,
|
||||
buildQuickEditGenerationInputs,
|
||||
buildUiDesignAssetExtractionGenerationInputs,
|
||||
buildVideoGenerationInputs,
|
||||
@@ -872,81 +870,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
],
|
||||
);
|
||||
|
||||
const addQuickEditResultLayer = useCallback(
|
||||
(
|
||||
generated: Parameters<typeof createQuickEditResultLayer>[0]['generated'],
|
||||
sourceLayer: CanvasLayer,
|
||||
generationInputs: CanvasGenerationInputs,
|
||||
mode: QuickEditPanelState['mode'] = 'quick-edit',
|
||||
options: {
|
||||
frame?: GenerateDialogState['placeholder'];
|
||||
dialogId?: string;
|
||||
title?: string;
|
||||
} = {},
|
||||
) => {
|
||||
if (
|
||||
options.dialogId &&
|
||||
!hasCanvasGenerationDialogById(options.dialogId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
layerCounterRef.current += 1;
|
||||
const generatedIndex = layerCounterRef.current;
|
||||
const nextLayer = createQuickEditResultLayer({
|
||||
generated,
|
||||
generatedIndex,
|
||||
sourceLayer,
|
||||
generationInputs,
|
||||
mode,
|
||||
frame: options.frame,
|
||||
title: options.title,
|
||||
});
|
||||
|
||||
addGeneratedLayersToCanvas([nextLayer]);
|
||||
selectSingleLayer(mode === 'redraw' ? sourceLayer.id : nextLayer.id);
|
||||
setActiveSidebarPanel('layers');
|
||||
if (mode === 'redraw') {
|
||||
setQuickEditPanel((currentPanel) =>
|
||||
currentPanel?.sourceLayerId === sourceLayer.id
|
||||
? {
|
||||
...currentPanel,
|
||||
mode: 'redraw',
|
||||
status: 'idle',
|
||||
errorMessage: undefined,
|
||||
}
|
||||
: currentPanel,
|
||||
);
|
||||
} else {
|
||||
setQuickEditPanel(null);
|
||||
setActiveTool('select');
|
||||
if (options.dialogId) {
|
||||
updateCanvasGenerationDialogById(
|
||||
options.dialogId,
|
||||
(currentDialog) => ({
|
||||
...currentDialog,
|
||||
status: 'idle',
|
||||
composerOpen: false,
|
||||
generatedLayerId: nextLayer.id,
|
||||
errorMessage: undefined,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
fitLayers([sourceLayer, nextLayer], { captureHistory: false });
|
||||
},
|
||||
[
|
||||
addGeneratedLayersToCanvas,
|
||||
fitLayers,
|
||||
layerCounterRef,
|
||||
selectSingleLayer,
|
||||
setActiveSidebarPanel,
|
||||
setActiveTool,
|
||||
setQuickEditPanel,
|
||||
updateCanvasGenerationDialogById,
|
||||
hasCanvasGenerationDialogById,
|
||||
],
|
||||
);
|
||||
|
||||
const applyQuickEditResultToSourceLayer = useCallback(
|
||||
(
|
||||
generated: Parameters<
|
||||
@@ -1507,15 +1430,11 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
return;
|
||||
}
|
||||
|
||||
const panelMode = quickEditPanel.mode ?? 'quick-edit';
|
||||
const quickEditResultTitle = resolveGenerationAssetLabel(
|
||||
quickEditPanel.assetLabel,
|
||||
`${quickEditSourceLayer.title} ${panelMode === 'redraw' ? '重绘' : '快速编辑'}`,
|
||||
`${quickEditSourceLayer.title} 快速编辑`,
|
||||
);
|
||||
const quickEditReferences =
|
||||
panelMode === 'redraw' || panelMode === 'quick-edit'
|
||||
? []
|
||||
: (quickEditPanel.quickEditReferences ?? []);
|
||||
const quickEditReferences: CharacterReferenceImage[] = [];
|
||||
const quickEditAspectRatio =
|
||||
quickEditPanel.aspectRatio ??
|
||||
inferEditorImageAspectRatio(
|
||||
@@ -1542,13 +1461,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
imageSize: quickEditImageSize,
|
||||
});
|
||||
const quickEditSize = `${quickEditOutputSize.width}x${quickEditOutputSize.height}`;
|
||||
const basePrompt =
|
||||
quickEditPanel.prompt.trim() ||
|
||||
(panelMode === 'redraw' ? '重绘图片' : '快速编辑图片');
|
||||
const normalizedPrompt = panelMode === 'redraw' ? basePrompt : basePrompt;
|
||||
const normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片';
|
||||
let quickEditDialogId: string | undefined;
|
||||
const shouldUseCanvasPlaceholder =
|
||||
panelMode === 'quick-edit' &&
|
||||
isVideoQuickEditSource(quickEditSourceLayer);
|
||||
if (shouldUseCanvasPlaceholder) {
|
||||
const quickEditDraft = createQuickEditGenerationDialogDraft({
|
||||
@@ -1594,21 +1509,11 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}
|
||||
|
||||
try {
|
||||
const needsExtraReferenceImageSrcs =
|
||||
panelMode === 'quick-edit' ||
|
||||
panelMode === 'redraw' ||
|
||||
isVideoQuickEditSource(quickEditSourceLayer);
|
||||
const extraReferenceImageSrcs = needsExtraReferenceImageSrcs
|
||||
? await Promise.all(
|
||||
quickEditReferences.map((reference) =>
|
||||
resolveEditorGenerationMediaReference(
|
||||
reference,
|
||||
'image',
|
||||
projectId,
|
||||
),
|
||||
),
|
||||
)
|
||||
: [];
|
||||
const extraReferenceImageSrcs = await Promise.all(
|
||||
quickEditReferences.map((reference) =>
|
||||
resolveEditorGenerationMediaReference(reference, 'image', projectId),
|
||||
),
|
||||
);
|
||||
const quickEditFrame = quickEditDialogId
|
||||
? getGeneratingDialogPlaceholder({
|
||||
id: quickEditDialogId,
|
||||
@@ -1625,10 +1530,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
originalWidth: quickEditOutputSize.width,
|
||||
originalHeight: quickEditOutputSize.height,
|
||||
});
|
||||
if (
|
||||
panelMode === 'quick-edit' &&
|
||||
isVideoQuickEditSource(quickEditSourceLayer)
|
||||
) {
|
||||
if (isVideoQuickEditSource(quickEditSourceLayer)) {
|
||||
const videoModel = DEFAULT_VIDEO_MODEL;
|
||||
const videoResolution = '480p' as const;
|
||||
const videoDurationSeconds = 4 as const;
|
||||
@@ -1712,7 +1614,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
return;
|
||||
}
|
||||
const quickEditSelectionMarks =
|
||||
panelMode === 'quick-edit' &&
|
||||
quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id
|
||||
? quickEditSelectionState.marks
|
||||
: [];
|
||||
@@ -1738,85 +1639,38 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
projectId,
|
||||
);
|
||||
}
|
||||
const generationInputs =
|
||||
panelMode === 'redraw'
|
||||
? buildEditGenerationInputs(
|
||||
'重绘提示词',
|
||||
normalizedPrompt,
|
||||
quickEditSourceLayer,
|
||||
{
|
||||
model: normalizedQuickEditModel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
},
|
||||
)
|
||||
: buildQuickEditGenerationInputs(
|
||||
'快速编辑提示词',
|
||||
normalizedPrompt,
|
||||
quickEditSourceLayer,
|
||||
quickEditReferences,
|
||||
{
|
||||
model: normalizedQuickEditModel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
},
|
||||
);
|
||||
const isCharacterRedraw =
|
||||
panelMode === 'redraw' &&
|
||||
quickEditSourceLayer.assetKind === 'character';
|
||||
const generated =
|
||||
panelMode === 'quick-edit'
|
||||
? await runEditorGenerationWithWalletRefresh(
|
||||
editEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
sourceImageSrc: sourceReferenceImageSrc,
|
||||
size: quickEditSize,
|
||||
model: normalizedQuickEditModel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
...(extraReferenceImageSrcs.length
|
||||
? { referenceImageSrcs: extraReferenceImageSrcs }
|
||||
: {}),
|
||||
projectId,
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: quickEditResultTitle,
|
||||
sourceResourceId: quickEditSourceLayer.resourceId,
|
||||
targetLayerId: quickEditSourceLayer.id,
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
)
|
||||
: await runEditorGenerationWithWalletRefresh(
|
||||
generateEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
size: quickEditSize,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
kind: isCharacterRedraw ? 'character' : 'quick-edit',
|
||||
model: normalizedQuickEditModel,
|
||||
referenceImageSrcs: [
|
||||
...extraReferenceImageSrcs,
|
||||
sourceReferenceImageSrc,
|
||||
],
|
||||
projectId,
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: quickEditResultTitle,
|
||||
sourceResourceId: quickEditSourceLayer.resourceId,
|
||||
...(projectId && quickEditCanvasCompletionPlaceholder
|
||||
? {
|
||||
canvasCompletion: {
|
||||
dialogId: quickEditDialogId,
|
||||
title: quickEditResultTitle,
|
||||
placeholder: quickEditCanvasCompletionPlaceholder,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
const generationInputs = buildQuickEditGenerationInputs(
|
||||
'快速编辑提示词',
|
||||
normalizedPrompt,
|
||||
quickEditSourceLayer,
|
||||
quickEditReferences,
|
||||
{
|
||||
model: normalizedQuickEditModel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
},
|
||||
);
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
editEditorImage({
|
||||
prompt: normalizedPrompt,
|
||||
sourceImageSrc: sourceReferenceImageSrc,
|
||||
size: quickEditSize,
|
||||
model: normalizedQuickEditModel,
|
||||
aspectRatio: quickEditAspectRatio,
|
||||
imageSize: quickEditImageSize,
|
||||
...(extraReferenceImageSrcs.length
|
||||
? { referenceImageSrcs: extraReferenceImageSrcs }
|
||||
: {}),
|
||||
projectId,
|
||||
assetKind: quickEditSourceLayer.assetKind,
|
||||
generationInputs,
|
||||
assetFolderId,
|
||||
assetLabel: quickEditResultTitle,
|
||||
sourceResourceId: quickEditSourceLayer.resourceId,
|
||||
targetLayerId: quickEditSourceLayer.id,
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
notifyEditorGenerationWarning(
|
||||
generated.warning?.reason,
|
||||
onGenerationWarning,
|
||||
@@ -1831,45 +1685,25 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
onGenerationWarning,
|
||||
)
|
||||
) {
|
||||
if (panelMode === 'quick-edit') {
|
||||
setQuickEditPanel(null);
|
||||
setActiveTool('select');
|
||||
setActiveSidebarPanel('layers');
|
||||
}
|
||||
setQuickEditPanel(null);
|
||||
setActiveTool('select');
|
||||
setActiveSidebarPanel('layers');
|
||||
return;
|
||||
}
|
||||
if (generated.project && applyProjectSnapshot) {
|
||||
applyProjectSnapshot(generated.project);
|
||||
if (panelMode === 'quick-edit') {
|
||||
setQuickEditPanel(null);
|
||||
setActiveTool('select');
|
||||
setActiveSidebarPanel('layers');
|
||||
}
|
||||
setQuickEditPanel(null);
|
||||
setActiveTool('select');
|
||||
setActiveSidebarPanel('layers');
|
||||
return;
|
||||
}
|
||||
if (panelMode === 'quick-edit') {
|
||||
applyQuickEditResultToSourceLayer(
|
||||
generated,
|
||||
quickEditSourceLayer,
|
||||
generationInputs,
|
||||
quickEditDialogId,
|
||||
quickEditResultTitle,
|
||||
);
|
||||
} else {
|
||||
addQuickEditResultLayer(
|
||||
generated,
|
||||
quickEditSourceLayer,
|
||||
generationInputs,
|
||||
panelMode,
|
||||
quickEditDialogId
|
||||
? {
|
||||
frame: quickEditFrame,
|
||||
dialogId: quickEditDialogId,
|
||||
title: quickEditResultTitle,
|
||||
}
|
||||
: { title: quickEditResultTitle },
|
||||
);
|
||||
}
|
||||
applyQuickEditResultToSourceLayer(
|
||||
generated,
|
||||
quickEditSourceLayer,
|
||||
generationInputs,
|
||||
quickEditDialogId,
|
||||
quickEditResultTitle,
|
||||
);
|
||||
} catch (error) {
|
||||
if (quickEditDialogId) {
|
||||
updateCanvasGenerationDialogById(
|
||||
@@ -1894,7 +1728,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
});
|
||||
}
|
||||
}, [
|
||||
addQuickEditResultLayer,
|
||||
addVideoResultLayer,
|
||||
applyQuickEditResultToSourceLayer,
|
||||
applyProjectSnapshot,
|
||||
@@ -2121,7 +1954,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
});
|
||||
} else if (submissionPlan.kind === 'quick-edit') {
|
||||
const isRedrawAsNewArtifact = Boolean(dialog.isRedrawAsNewArtifact);
|
||||
const quickEditSelectionMarks =
|
||||
quickEditSelectionState?.sourceLayerId ===
|
||||
submissionPlan.sourceLayer.id
|
||||
@@ -2166,11 +1998,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
),
|
||||
),
|
||||
);
|
||||
const quickEditPlaceholderSize =
|
||||
getCanvasCompletionPlaceholderSizeFromPlan({
|
||||
sourceLayer: submissionPlan.sourceLayer,
|
||||
outputSize: submissionPlan.editInput.size,
|
||||
});
|
||||
const generated = await runEditorGenerationWithWalletRefresh(
|
||||
editEditorImage({
|
||||
prompt: submissionPlan.normalizedPrompt,
|
||||
@@ -2185,25 +2012,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
assetFolderId,
|
||||
assetLabel: submissionPlan.result.title,
|
||||
sourceResourceId: submissionPlan.sourceLayer.resourceId,
|
||||
...(isRedrawAsNewArtifact
|
||||
? {
|
||||
canvasCompletion: {
|
||||
dialogId: canvasDialog?.id,
|
||||
title: submissionPlan.result.title,
|
||||
placeholder:
|
||||
getGeneratingDialogPlaceholder(dialog) ??
|
||||
buildRightSideCanvasCompletionPlaceholder(
|
||||
submissionPlan.sourceLayer,
|
||||
{
|
||||
width: quickEditPlaceholderSize.width,
|
||||
height: quickEditPlaceholderSize.height,
|
||||
originalWidth: quickEditPlaceholderSize.width,
|
||||
originalHeight: quickEditPlaceholderSize.height,
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
: { targetLayerId: submissionPlan.sourceLayer.id }),
|
||||
targetLayerId: submissionPlan.sourceLayer.id,
|
||||
}),
|
||||
onWalletBalanceMayHaveChanged,
|
||||
);
|
||||
@@ -2225,21 +2034,13 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isRedrawAsNewArtifact) {
|
||||
addGeneratedResultLayer(generated, {
|
||||
sourceLayer: submissionPlan.sourceLayer,
|
||||
title: submissionPlan.result.title,
|
||||
generationInputs: submissionPlan.result.generationInputs,
|
||||
});
|
||||
} else {
|
||||
applyQuickEditResultToSourceLayer(
|
||||
generated,
|
||||
submissionPlan.sourceLayer,
|
||||
submissionPlan.result.generationInputs,
|
||||
canvasDialog?.id,
|
||||
submissionPlan.result.title,
|
||||
);
|
||||
}
|
||||
applyQuickEditResultToSourceLayer(
|
||||
generated,
|
||||
submissionPlan.sourceLayer,
|
||||
submissionPlan.result.generationInputs,
|
||||
canvasDialog?.id,
|
||||
submissionPlan.result.title,
|
||||
);
|
||||
} else if (submissionPlan.kind === 'icon-spec') {
|
||||
const iconSpecInput = submissionPlan.input;
|
||||
const canvasCompletionPlaceholder =
|
||||
|
||||
@@ -500,6 +500,14 @@ function GenerationWorkflowHarness({
|
||||
)
|
||||
.join('|')}
|
||||
</span>
|
||||
<span data-testid="layer-placements">
|
||||
{layers
|
||||
.map(
|
||||
(layer) =>
|
||||
`${layer.id}:${layer.x}:${layer.y}:${layer.width}:${layer.height}`,
|
||||
)
|
||||
.join('|')}
|
||||
</span>
|
||||
<span data-testid="dialog">
|
||||
{activeDialog
|
||||
? `${activeDialog.mode}:${activeDialog.status}:${activeDialog.composerOpen !== false ? 'open' : 'closed'}:${activeDialog.generatedLayerId ?? '-'}:${activeDialog.placeholder ? 'placeholder' : '-'}`
|
||||
@@ -563,7 +571,7 @@ function GenerationWorkflowHarness({
|
||||
</span>
|
||||
<span data-testid="quick-edit-image-options">
|
||||
{workflow.quickEditPanel
|
||||
? `${workflow.quickEditPanel.mode ?? 'quick-edit'}:${workflow.quickEditPanel.model}:${workflow.quickEditPanel.aspectRatio ?? '-'}:${workflow.quickEditPanel.imageSize ?? '-'}`
|
||||
? `quick-edit:${workflow.quickEditPanel.model}:${workflow.quickEditPanel.aspectRatio ?? '-'}:${workflow.quickEditPanel.imageSize ?? '-'}`
|
||||
: '-'}
|
||||
</span>
|
||||
<span data-testid="crop-expand">
|
||||
@@ -1563,14 +1571,14 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('opens generated layer remodel with user input snapshot instead of internal prompt', () => {
|
||||
it('opens legacy image remodel with user input snapshot in the default image tool', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开用户快照改造' }));
|
||||
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'quick-edit:idle:open:-:placeholder',
|
||||
'generate:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('dialog-prompt').textContent).toBe(
|
||||
'用户的视频描述',
|
||||
@@ -1592,19 +1600,176 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('opens image redraw as a new canvas generator next to the source layer', () => {
|
||||
render(<GenerationWorkflowHarness />);
|
||||
it('submits legacy image remodel without an origin action through the default image tool', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '旧版森林场景' }),
|
||||
);
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
sourceType: 'generated',
|
||||
prompt: '【系统内置】旧版森林场景完整提示词',
|
||||
model: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
generationInputs: {
|
||||
fields: [{ title: '生成提示词', value: '旧版森林场景' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开生成' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开图片改造' }));
|
||||
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'quick-edit:idle:open:-:placeholder',
|
||||
'generate:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
||||
expect(screen.getByTestId('placeholder').textContent).toContain('1024:768');
|
||||
const expectedResultPlacement =
|
||||
screen.getByTestId('placeholder').textContent;
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '旧版森林场景',
|
||||
model: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
generationInputs: expect.objectContaining({
|
||||
version: 2,
|
||||
action: 'image.generate',
|
||||
references: [],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
const request = generateEditorImageMock.mock.calls[0]?.[0];
|
||||
expect(request).not.toHaveProperty('referenceImageSrcs');
|
||||
expect(request).not.toHaveProperty('sourceResourceId');
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-generated-1:生成图片 1:-:-',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('layer-placements').textContent).toContain(
|
||||
`layer-generated-1:${expectedResultPlacement}`,
|
||||
);
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'generate:idle:open:layer-generated-1:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('generation-dialogs').textContent).toBe(
|
||||
'generation-dialog-1:generate:closed|generation-dialog-2:quick-edit:open',
|
||||
'generation-dialog-1:generate:open',
|
||||
);
|
||||
});
|
||||
|
||||
it('submits legacy character remodel through the character tool selected by asset kind', async () => {
|
||||
generateEditorImageMock.mockResolvedValueOnce(
|
||||
createGenerated({ prompt: '红发骑士' }),
|
||||
);
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
title: '角色形象',
|
||||
sourceType: 'generated',
|
||||
assetKind: 'character',
|
||||
prompt: '【系统内置】红发骑士完整提示词',
|
||||
model: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
generationInputs: {
|
||||
fields: [{ title: '角色设定', value: '红发骑士' }],
|
||||
references: [],
|
||||
},
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开图片改造' }));
|
||||
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'character:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('dialog-prompt').textContent).toBe('红发骑士');
|
||||
const expectedResultPlacement =
|
||||
screen.getByTestId('placeholder').textContent;
|
||||
fireEvent.click(screen.getByRole('button', { name: '选择角色规范' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交生成' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(generateEditorImageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: '红发骑士',
|
||||
kind: 'character',
|
||||
model: IMAGE_MODEL_GPT_IMAGE_2,
|
||||
referenceImageSrcs: ['resource-character-spec'],
|
||||
generationInputs: expect.objectContaining({
|
||||
version: 2,
|
||||
action: 'character.generate',
|
||||
references: [
|
||||
expect.objectContaining({
|
||||
id: 'specReference',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-character-spec',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('layers').textContent).toContain(
|
||||
'layer-generated-1:角色形象 1:-:character',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('layer-placements').textContent).toContain(
|
||||
`layer-generated-1:${expectedResultPlacement}`,
|
||||
);
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'character:idle:open:layer-generated-1:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('generation-dialogs').textContent).toBe(
|
||||
'generation-dialog-1:character:open',
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects image.edit remodel even when the internal callback is invoked', () => {
|
||||
render(
|
||||
<GenerationWorkflowHarness
|
||||
initialLayers={[
|
||||
createLayer({
|
||||
id: 'layer-edit-result',
|
||||
resourceId: 'resource-edit-result',
|
||||
sourceType: 'generated',
|
||||
generationInputs: {
|
||||
version: 2,
|
||||
action: 'image.edit',
|
||||
fields: [
|
||||
{ id: 'prompt', title: '修改要求', value: '改成雨天' },
|
||||
{ id: 'model', title: '模型', value: 'gpt-image-2' },
|
||||
{ id: 'aspectRatio', title: '比例', value: '4:3' },
|
||||
{ id: 'imageSize', title: '尺寸', value: '1K' },
|
||||
],
|
||||
references: [
|
||||
{
|
||||
id: 'source',
|
||||
title: '原图',
|
||||
refType: 'project-resource',
|
||||
refId: 'resource-edit-source',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开图片改造' }));
|
||||
|
||||
expect(screen.getByTestId('dialog').textContent).toBe('-');
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
||||
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
|
||||
'快速编辑结果不支持改造,请继续使用快速编辑。',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5150,7 +5315,10 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '打开图片改造' }));
|
||||
|
||||
expect(screen.getByTestId('dialog').textContent).not.toBe('-');
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'generate:idle:open:-:placeholder',
|
||||
);
|
||||
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
|
||||
expect(screen.getByTestId('reference-pick-warning').textContent).toBe(
|
||||
'已按旧版数据恢复,部分参数可能使用当前默认值。',
|
||||
);
|
||||
|
||||
@@ -69,10 +69,10 @@ import {
|
||||
createEditDialogDraft,
|
||||
createGenerateDialogDraft,
|
||||
createIconGenerationDialogDraft,
|
||||
createLegacyImageRedrawGenerationDialogDraft,
|
||||
createPublicationGenerationDialogDraft,
|
||||
createQuickEditGenerationDialogDraft,
|
||||
createQuickEditPanelDraft,
|
||||
createRedrawPanelDraft,
|
||||
createSameSourceGenerationDialogDraft,
|
||||
createSceneGenerationDialogDraft,
|
||||
createSoundEffectGenerationDialogDraft,
|
||||
@@ -105,7 +105,6 @@ import {
|
||||
normalizeEditorImageModel,
|
||||
resizeGenerationPlaceholderToImageSelection,
|
||||
resolveDialogExtraImageReferenceLimit,
|
||||
resolveEditorImageGenerationPixelSize,
|
||||
resolveExtraImageReferenceLimit,
|
||||
UI_EXTRA_REFERENCE_LIMIT,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
@@ -1084,14 +1083,15 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const refreshTaskList = useCallback(() => {
|
||||
setTaskListRefreshKey((key) => key + 1);
|
||||
}, []);
|
||||
const backgroundMusicPromptAssist =
|
||||
useImageCanvasBackgroundMusicPromptAssist({
|
||||
const backgroundMusicPromptAssist = useImageCanvasBackgroundMusicPromptAssist(
|
||||
{
|
||||
canvasGenerationDialogs,
|
||||
getCanvasGenerationDialogById,
|
||||
updateCanvasGenerationDialogById,
|
||||
currentUserId,
|
||||
projectId,
|
||||
});
|
||||
},
|
||||
);
|
||||
const soundEffectPromptAssist = useImageCanvasSoundEffectPromptAssist({
|
||||
canvasGenerationDialogs,
|
||||
getCanvasGenerationDialogById,
|
||||
@@ -1850,10 +1850,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
);
|
||||
const hasInvalidVersionedGenerationInputs = Boolean(
|
||||
sourceLayer.generationInputsHydrationState === 'invalid-versioned' ||
|
||||
sourceLayer.generationInputs &&
|
||||
(sourceLayer.generationInputs.version !== undefined ||
|
||||
sourceLayer.generationInputs.action !== undefined) &&
|
||||
!decodedGenerationInputs.ok,
|
||||
(sourceLayer.generationInputs &&
|
||||
(sourceLayer.generationInputs.version !== undefined ||
|
||||
sourceLayer.generationInputs.action !== undefined) &&
|
||||
!decodedGenerationInputs.ok),
|
||||
);
|
||||
if (hasInvalidVersionedGenerationInputs) {
|
||||
showGenerationWarning(
|
||||
@@ -1865,6 +1865,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
? decodedGenerationInputs.inputs
|
||||
: null;
|
||||
const isNormalizedGenerationInputs = normalizedGenerationInputs !== null;
|
||||
if (normalizedGenerationInputs?.action === 'image.edit') {
|
||||
showGenerationWarning('快速编辑结果不支持改造,请继续使用快速编辑。');
|
||||
return;
|
||||
}
|
||||
const hasParameterFallbacks = decodedGenerationInputs.ok
|
||||
? decodedGenerationInputs.warnings.length > 0
|
||||
: false;
|
||||
@@ -1994,9 +1998,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
selectSingleLayer(sourceLayer.id);
|
||||
openCanvasGenerationDialog(audioDraft);
|
||||
setActiveTool('music');
|
||||
showGenerationWarning(
|
||||
'已按旧版数据恢复,部分参数可能使用当前默认值。',
|
||||
);
|
||||
showGenerationWarning('已按旧版数据恢复,部分参数可能使用当前默认值。');
|
||||
return;
|
||||
}
|
||||
if (sourceLayer.mediaType === 'video') {
|
||||
@@ -2008,9 +2010,7 @@ export function useImageCanvasGenerationWorkflow({
|
||||
selectSingleLayer(sourceLayer.id);
|
||||
openCanvasGenerationDialog(videoDraft);
|
||||
setActiveTool('video');
|
||||
showGenerationWarning(
|
||||
'已按旧版数据恢复,部分参数可能使用当前默认值。',
|
||||
);
|
||||
showGenerationWarning('已按旧版数据恢复,部分参数可能使用当前默认值。');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
@@ -2022,36 +2022,18 @@ export function useImageCanvasGenerationWorkflow({
|
||||
showGenerationWarning('未找到角色动作关联的原角色图层,无法改造。');
|
||||
return;
|
||||
}
|
||||
const redrawPanel = createRedrawPanelDraft(sourceLayer, {
|
||||
imageModel: sourceDialog?.imageModel,
|
||||
aspectRatio: sourceDialog?.aspectRatio,
|
||||
imageSize: sourceDialog?.imageSize,
|
||||
});
|
||||
const redrawSize = resolveEditorImageGenerationPixelSize({
|
||||
model: redrawPanel.model,
|
||||
aspectRatio: redrawPanel.aspectRatio,
|
||||
imageSize: redrawPanel.imageSize,
|
||||
});
|
||||
openPlacedCanvasGenerationDialog({
|
||||
...createQuickEditGenerationDialogDraft({
|
||||
openPlacedCanvasGenerationDialog(
|
||||
createLegacyImageRedrawGenerationDialogDraft({
|
||||
sourceLayer,
|
||||
prompt: redrawPanel.prompt,
|
||||
model: redrawPanel.model,
|
||||
aspectRatio: redrawPanel.aspectRatio,
|
||||
imageSize: redrawPanel.imageSize,
|
||||
frame: {
|
||||
width: redrawSize.width,
|
||||
height: redrawSize.height,
|
||||
},
|
||||
canvasSize,
|
||||
viewport,
|
||||
sourceDialog,
|
||||
}),
|
||||
composerOpen: true,
|
||||
});
|
||||
);
|
||||
setQuickEditPanel(null);
|
||||
selectSingleLayer(null);
|
||||
setActiveTool('generate');
|
||||
showGenerationWarning(
|
||||
'已按旧版数据恢复,部分参数可能使用当前默认值。',
|
||||
);
|
||||
showGenerationWarning('已按旧版数据恢复,部分参数可能使用当前默认值。');
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user