修复画布快速编辑回写与原图元数据

快速编辑回到原图面板流程,避免新建生成器占位

提交快速编辑时传入目标图层并在后端成功后回写画布

快速编辑生成输入记录原图引用,图片信息可展示原图来源

同步前后端测试与图片画布文档记忆
This commit is contained in:
2026-07-02 23:05:38 +08:00
parent 7d954e0985
commit d707613a8b
27 changed files with 528 additions and 323 deletions
@@ -183,6 +183,48 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
expect(submitButton.textContent).toBe('生成5泥点');
});
it('keeps quick edit to one prompt box and model selection', () => {
render(
<BasicGenerationHarness
initialDialog={createDialog({
mode: 'quick-edit',
sourceLayerId: 'layer-source',
generationReferences: [
{
id: 'ref-a',
label: '旧参考图',
src: 'data:image/png;base64,cmVmQQ==',
},
],
imageModel: 'gpt-image-2',
aspectRatio: '1:1',
imageSize: '1K',
})}
/>,
);
const panel = screen.getByRole('dialog', { name: '快速编辑图片' });
expect(
within(panel).getByRole('textbox', { name: '快速编辑提示词' }),
).toBeTruthy();
expect(
within(panel).queryByRole('button', { name: '添加参考图' }),
).toBeNull();
expect(within(panel).queryByText('旧参考图')).toBeNull();
expect(
within(panel).queryByRole('button', {
name: /快速编辑图片尺寸/u,
}),
).toBeNull();
expect(
within(panel).getByRole('button', {
name: '快速编辑图片模型 gpt-image-2',
}),
).toBeTruthy();
expect(within(panel).getByRole('button', { name: '修改' })).toBeTruthy();
});
it('clicks the parent generation panel to collapse an opened option panel', () => {
render(<BasicGenerationHarness />);
@@ -145,13 +145,23 @@ export function ImageCanvasBasicGenerationComposerView({
promptLabel ?? (isQuickEdit ? '快速编辑提示词' : '生成提示词');
const resolvedPromptPlaceholder =
promptPlaceholder ??
(isQuickEdit ? '想怎么编辑这张图?' : '今天想生成什么画面?');
(isQuickEdit ? '写下每个编号要怎么改' : '今天想生成什么画面?');
const resolvedOptionLabelPrefix =
optionLabelPrefix ?? (isQuickEdit ? '快速编辑图片' : '生成图片');
const resolvedReferenceButtonLabel = referenceButtonLabel;
const resolvedReferenceButtonAriaLabel =
referenceButtonAriaLabel ?? `添加${resolvedReferenceButtonLabel}`;
const hasReferenceMenu = includeReferences && onRequestUpload;
const shouldIncludeReferences = isQuickEdit ? false : includeReferences;
const shouldIncludeDimensions = isQuickEdit ? false : includeDimensions;
const resolvedSubmitLabel =
isQuickEdit && submitLabel === '生成' ? '修改' : submitLabel;
const resolvedSubmitAriaLabel =
isQuickEdit && submitAriaLabel === '生成' ? '修改' : submitAriaLabel;
const resolvedSubmittingStatusLabel =
isQuickEdit && submittingStatusLabel === '生成中'
? '修改中'
: submittingStatusLabel;
const hasReferenceMenu = shouldIncludeReferences && onRequestUpload;
const finalFormClassName =
formClassName ??
'image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image';
@@ -179,7 +189,7 @@ export function ImageCanvasBasicGenerationComposerView({
}
}}
>
{includeReferences ? (
{shouldIncludeReferences ? (
<div className="image-canvas-editor__reference-strip">
{references.map((reference, index) => {
const label =
@@ -259,7 +269,7 @@ export function ImageCanvasBasicGenerationComposerView({
<ImageCanvasGenerationImageOptionsView
dialog={dialog}
setGenerateDialog={setGenerateDialog}
includeDimensions={includeDimensions}
includeDimensions={shouldIncludeDimensions}
includeModel={includeModel}
onRememberImageModel={onRememberImageModel}
dimensionRatioAriaLabelPrefix={dimensionRatioAriaLabelPrefix}
@@ -269,8 +279,8 @@ export function ImageCanvasBasicGenerationComposerView({
model: dialog.imageModel,
imageSize: dialog.imageSize,
})}
submitLabel={submitLabel}
submitAriaLabel={submitAriaLabel}
submitLabel={resolvedSubmitLabel}
submitAriaLabel={resolvedSubmitAriaLabel}
submitButtonClassName={submitButtonClassName}
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
@@ -284,7 +294,7 @@ export function ImageCanvasBasicGenerationComposerView({
className="image-canvas-editor__generate-status"
role="status"
>
{submittingStatusLabel}
{resolvedSubmittingStatusLabel}
</PlatformStatusMessage>
) : null}
{dialog.status === 'failed' ? (
@@ -566,14 +566,12 @@ describe('ImageCanvasEditorView generation integration', () => {
fireEvent.click(screen.getByRole('button', { name: '快速编辑' }));
expect(
await screen.findByRole('dialog', { name: '生成角色形象' }),
await screen.findByRole('dialog', { name: '快速编辑图片' }),
).toBeTruthy();
expect(
(screen.getByLabelText('角色设定') as HTMLTextAreaElement).value,
).toBe('黑衣剑士');
expect(screen.queryByRole('dialog', { name: '生成角色形象' })).toBeNull();
});
it('opens the matching generator from image context quick edit for tagged uploaded images', async () => {
it('opens quick edit from image context for tagged uploaded images', async () => {
loadOrCreateRecentEditorProjectMock.mockResolvedValueOnce({
projectId: 'editor-project-context-tagged-layer',
title: '右键标签素材画布',
@@ -617,13 +615,10 @@ describe('ImageCanvasEditorView generation integration', () => {
),
);
expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull();
expect(
await screen.findByRole('dialog', { name: '生成角色形象' }),
await screen.findByRole('dialog', { name: '快速编辑图片' }),
).toBeTruthy();
expect(
(screen.getByLabelText('角色设定') as HTMLTextAreaElement).value,
).toBe('红披风骑士');
expect(screen.queryByRole('dialog', { name: '生成角色形象' })).toBeNull();
});
it('restores publication material composer fields and references from saved generator snapshots', async () => {
@@ -1161,6 +1161,7 @@ export function ImageCanvasEditorView() {
setIsPickingUiDesignSpecFromCanvas,
openCharacterAnimationPanel,
openRedrawPanel,
openQuickEditPanel,
openCropExpandPanel,
removeSelectedLayerBackground,
extractUiDesignAssets,
@@ -1903,7 +1904,7 @@ export function ImageCanvasEditorView() {
onFocusExternalTask: focusExternalGenerationTask,
onToggleTaskSidebar: generationSurface.toggleTaskSidebar,
onCropExpandHandlePointerDown: generationSurface.startCropExpandFrameResize,
onOpenQuickEditPanel: openLayerGenerationDialog,
onOpenQuickEditPanel: openQuickEditPanel,
onOpenRedrawPanel: openRedrawPanel,
onOpenCropExpandPanel: openCropExpandPanel,
onRemoveBackground: removeSelectedLayerBackground,
@@ -117,7 +117,7 @@ function renderComposer(
}
describe('ImageCanvasGenerationComposerView', () => {
it('让快速编辑复用普通图片生成面板结构', () => {
it('让快速编辑只保留提示词和模型选择', () => {
renderComposer({
mode: 'quick-edit',
prompt: '',
@@ -142,6 +142,20 @@ describe('ImageCanvasGenerationComposerView', () => {
);
expect(within(panel).getByRole('textbox', { name: '快速编辑提示词' }))
.toBeTruthy();
expect(
within(panel).queryByRole('button', { name: '添加参考图' }),
).toBeNull();
expect(
within(panel).queryByRole('button', {
name: /快速编辑图片尺寸/u,
}),
).toBeNull();
expect(
within(panel).getByRole('button', {
name: '快速编辑图片模型 gpt-image-2',
}),
).toBeTruthy();
expect(within(panel).getByRole('button', { name: '修改' })).toBeTruthy();
expect(
panel.querySelector('.image-canvas-editor__generation-composer-footer'),
).toBeTruthy();
@@ -1198,11 +1198,7 @@ export function ImageCanvasGenerationComposerView({
</div>
) : null}
{quickEditPanel &&
(quickEditPanel.mode === 'redraw' ||
quickEditPanel.status !== 'generating') &&
quickEditSourceLayer &&
quickEditPanelStyle ? (
{quickEditPanel && quickEditSourceLayer && quickEditPanelStyle ? (
<ImageCanvasQuickEditPanelView
panel={quickEditPanel}
sourceLayer={quickEditSourceLayer}
@@ -37,7 +37,7 @@ import {
updateSpecFormDialogValue,
} from './ImageCanvasGenerationDialogModel';
import {
IMAGE_MODEL_NANOBANANA2,
IMAGE_MODEL_GPT_IMAGE_2,
} from './ImageCanvasGenerationModel';
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
@@ -649,21 +649,21 @@ describe('ImageCanvasGenerationDialogModel', () => {
});
});
it('normalizes legacy nanobanana model aliases in quick edit drafts', () => {
it('locks quick edit drafts to gpt-image-2 despite legacy source models', () => {
expect(
createQuickEditPanelDraft(
createLayer({
model: 'nano-banana',
}),
).model,
).toBe(IMAGE_MODEL_NANOBANANA2);
).toBe(IMAGE_MODEL_GPT_IMAGE_2);
expect(
createQuickEditPanelDraft(
createLayer({
model: 'nanobanana2',
}),
).model,
).toBe(IMAGE_MODEL_NANOBANANA2);
).toBe(IMAGE_MODEL_GPT_IMAGE_2);
});
it('creates character animation generation dialog drafts for character layers', () => {
@@ -1187,9 +1187,7 @@ export function createQuickEditPanelDraft(
sourceLayer.originalWidth,
sourceLayer.originalHeight,
);
const model = normalizeEditorImageModel(
options.imageModel ?? sourceLayer.model,
);
const model = IMAGE_MODEL_GPT_IMAGE_2;
return {
mode: 'quick-edit',
sourceLayerId: sourceLayer.id,
@@ -1211,7 +1209,6 @@ export function createQuickEditGenerationDialogDraft({
prompt,
status = 'idle',
references = [],
model,
aspectRatio,
imageSize,
frame,
@@ -1234,7 +1231,7 @@ export function createQuickEditGenerationDialogDraft({
composerOpen: false,
sourceLayerId: sourceLayer.id,
generationReferences: references.slice(0, 8),
imageModel: normalizeEditorImageModel(model ?? sourceLayer.model),
imageModel: IMAGE_MODEL_GPT_IMAGE_2,
aspectRatio,
imageSize,
placeholder: {
@@ -1259,6 +1256,7 @@ export function createRedrawPanelDraft(
return {
...createQuickEditPanelDraft(sourceLayer, options),
mode: 'redraw',
model: normalizeEditorImageModel(options.imageModel ?? sourceLayer.model),
prompt: resolveUserGenerationPromptSnapshot(sourceLayer),
};
}
@@ -19,6 +19,7 @@ import {
getEditorImageModelDisplayName,
IMAGE_MODEL_NANOBANANA2,
normalizeEditorImageModel,
QUICK_EDIT_MODEL_OPTIONS,
resizeGenerationPlaceholderToImageSelection,
resolveEditorImageSizeLabel,
} from './ImageCanvasGenerationModel';
@@ -135,6 +136,7 @@ export function ImageCanvasGenerationImageOptionsView({
const [openPanel, setOpenPanel] = useState<OpenPanel>(null);
const dimensionsButtonRef = useRef<HTMLButtonElement | null>(null);
const modelButtonRef = useRef<HTMLButtonElement | null>(null);
const isQuickEdit = dialog.mode === 'quick-edit';
const normalizedLockedModel = lockedModel
? normalizeEditorImageModel(lockedModel)
: null;
@@ -143,7 +145,17 @@ export function ImageCanvasGenerationImageOptionsView({
? { ...dialog, imageModel: normalizedLockedModel }
: dialog,
);
const selectedModelLabel = getEditorImageModelDisplayName(selection.model);
const modelOptions = isQuickEdit
? [...QUICK_EDIT_MODEL_OPTIONS]
: [...EDITOR_IMAGE_MODEL_OPTIONS];
const selectedModel = modelOptions.some(
(option) => option.value === selection.model,
)
? selection.model
: (modelOptions[0]?.value ?? selection.model);
const selectedModelLabel =
modelOptions.find((option) => option.value === selectedModel)?.label ??
getEditorImageModelDisplayName(selectedModel);
const selectedSizeLabel = resolveEditorImageSizeLabel({
aspectRatio: selection.aspectRatio,
imageSize: selection.imageSize,
@@ -340,8 +352,8 @@ export function ImageCanvasGenerationImageOptionsView({
onPointerDown={(event) => event.stopPropagation()}
>
<div className="image-canvas-editor__option-popover-items image-canvas-editor__option-popover-items--model">
{EDITOR_IMAGE_MODEL_OPTIONS.map((option) => {
const selected = selection.model === option.value;
{modelOptions.map((option) => {
const selected = selectedModel === option.value;
return (
<OptionChoice
key={option.value}
@@ -348,6 +348,12 @@ describe('ImageCanvasGenerationModel', () => {
).toEqual({
fields: [{ title: '快速编辑提示词', value: '让图2下雨' }],
references: [
{
title: '原图',
label: '原图',
refType: 'project-resource',
refId: 'resource-source',
},
{
title: '参考图 1',
label: '天空',
@@ -373,11 +379,9 @@ describe('ImageCanvasGenerationModel', () => {
buildSpecPrompt('custom', { ...blankSpecValues, customPrompt: '自定义' }),
).toBe('自定义');
expect(buildQuickEditModelOptions('nano-banana')).toEqual([
{ label: 'nanobanana2', value: DEFAULT_IMAGE_MODEL },
{ label: 'gpt-image-2', value: 'gpt-image-2' },
]);
expect(buildQuickEditModelOptions('nanobanana2')).toEqual([
{ label: 'nanobanana2', value: DEFAULT_IMAGE_MODEL },
{ label: 'gpt-image-2', value: 'gpt-image-2' },
]);
});
@@ -105,11 +105,13 @@ export const QUICK_EDIT_SIZE_PRESETS = [
'2048x1152',
'1024x1536',
] as const;
export const QUICK_EDIT_MODEL_OPTIONS = [
export const EDITOR_IMAGE_MODEL_OPTIONS = [
{ label: 'nanobanana2', value: IMAGE_MODEL_NANOBANANA2 },
{ label: 'gpt-image-2', value: IMAGE_MODEL_GPT_IMAGE_2 },
] as const;
export const EDITOR_IMAGE_MODEL_OPTIONS = QUICK_EDIT_MODEL_OPTIONS;
export const QUICK_EDIT_MODEL_OPTIONS = [
{ label: 'gpt-image-2', value: IMAGE_MODEL_GPT_IMAGE_2 },
] as const;
export const EDITOR_IMAGE_DIMENSION_OPTIONS = {
[IMAGE_MODEL_NANOBANANA2]: {
aspectRatios: ['1:1', '4:3', '3:2', '2:3', '9:16', '16:9'],
@@ -548,14 +550,9 @@ export function getEditorImageModelDisplayName(
}
export function buildQuickEditModelOptions(currentModel: string) {
const normalizedCurrentModel = normalizeEditorImageModel(currentModel);
void currentModel;
const options = [...QUICK_EDIT_MODEL_OPTIONS];
return options.some((option) => option.value === normalizedCurrentModel)
? options
: [
{ label: normalizedCurrentModel, value: normalizedCurrentModel },
...options,
];
return options;
}
export function buildCharacterSpecPrompt(values: SpecFormValues) {
@@ -1233,7 +1230,7 @@ export function buildEditGenerationInputs(
export function buildQuickEditGenerationInputs(
title: '快速编辑提示词' | '重绘提示词',
prompt: string,
_sourceLayer: CanvasLayer,
sourceLayer: CanvasLayer,
references: CharacterReferenceImage[] | undefined,
): CanvasGenerationInputs {
const limitedReferences = (references ?? []).slice(
@@ -1243,6 +1240,7 @@ export function buildQuickEditGenerationInputs(
return {
fields: createGenerationInputField(title, prompt),
references: [
...createLayerGenerationInputReference('原图', sourceLayer),
...limitedReferences.flatMap((reference, index) =>
createGenerationInputReference(`参考图 ${index + 1}`, reference),
),
@@ -158,7 +158,14 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
assetKind: 'character',
generationInputs: {
fields: [{ title: '快速编辑提示词', value: '把当前图改成雨天' }],
references: [],
references: [
{
title: '原图',
label: '源图',
refType: 'project-resource',
refId: 'resource-source',
},
],
},
},
rememberImageModel: IMAGE_MODEL_GPT_IMAGE_2,
@@ -256,10 +256,9 @@ export function buildImageGenerationSubmissionPlan({
if (!sourceLayer) {
throw new Error('未找到要改造的图片');
}
const quickEditReferences = dialog.generationReferences ?? [];
const basePrompt = dialog.prompt.trim() || '快速编辑图片';
const normalizedQuickEditPrompt = basePrompt;
const imageModel = normalizeEditorImageModel(dialog.imageModel);
const imageModel = IMAGE_MODEL_GPT_IMAGE_2;
const aspectRatio =
dialog.aspectRatio ??
inferEditorImageAspectRatio(
@@ -292,7 +291,7 @@ export function buildImageGenerationSubmissionPlan({
'快速编辑提示词',
normalizedQuickEditPrompt,
sourceLayer,
quickEditReferences,
[],
),
},
rememberImageModel: imageModel,
@@ -81,7 +81,7 @@ function QuickEditPanelHarness({
}
describe('ImageCanvasQuickEditPanelView', () => {
it('updates prompt and clears failed state while keeping options editable', () => {
it('updates prompt and clears failed state while keeping model editable', () => {
const rememberImageModel = vi.fn();
render(
<QuickEditPanelHarness
@@ -103,24 +103,22 @@ describe('ImageCanvasQuickEditPanelView', () => {
target: { value: '新提示' },
});
expect(
screen.getByPlaceholderText('写下每个编号要怎么改'),
).toBeTruthy();
expect(screen.getByLabelText('当前提示词').textContent).toBe('新提示');
expect(screen.getByLabelText('当前尺寸').textContent).toBe('1024x1024');
expect(screen.getByLabelText('当前模型').textContent).toBe(
'gemini-3.1-flash-image-preview',
);
expect(screen.getByLabelText('当前模型').textContent).toBe('gpt-image-2');
expect(screen.getByLabelText('当前状态').textContent).toBe('idle');
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
fireEvent.click(
screen.getByRole('button', { name: '快速编辑尺寸 1:1·1K' }),
);
fireEvent.click(screen.getByRole('button', { name: '比例 16:9' }));
fireEvent.click(screen.getByRole('button', { name: '尺寸 2K' }));
expect(screen.getByLabelText('当前比例').textContent).toBe('16:9');
expect(screen.getByLabelText('当前图片规格').textContent).toBe('2K');
expect(
screen.queryByRole('button', { name: '快速编辑尺寸 1:1·1K' }),
).toBeNull();
fireEvent.click(
screen.getByRole('button', { name: '快速编辑模型 nanobanana2' }),
screen.getByRole('button', { name: '快速编辑模型 gpt-image-2' }),
);
expect(screen.queryByRole('button', { name: 'nanobanana2' })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'gpt-image-2' }));
expect(screen.getByLabelText('当前模型').textContent).toBe('gpt-image-2');
expect(rememberImageModel).toHaveBeenCalledWith('gpt-image-2');
@@ -143,7 +141,7 @@ describe('ImageCanvasQuickEditPanelView', () => {
/>,
);
fireEvent.click(screen.getByRole('button', { name: '生成' }));
fireEvent.click(screen.getByRole('button', { name: '修改' }));
expect(submitQuickEdit).toHaveBeenCalledTimes(1);
expect(
@@ -172,7 +170,7 @@ describe('ImageCanvasQuickEditPanelView', () => {
);
});
it('renders only extra references and keeps the implicit source image out of the strip', () => {
it('hides quick edit references from the simplified panel', () => {
render(
<QuickEditPanelHarness
initialPanel={{
@@ -191,18 +189,15 @@ describe('ImageCanvasQuickEditPanelView', () => {
/>,
);
expect(screen.getByLabelText('图1')).toBeTruthy();
expect(screen.queryByLabelText('图1')).toBeNull();
expect(screen.queryByLabelText('原图 图2')).toBeNull();
fireEvent.click(screen.getByRole('button', { name: '删除图1' }));
expect(screen.getByLabelText('参考图数量').textContent).toBe('0');
expect(screen.getByLabelText('当前状态').textContent).toBe('idle');
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
expect(screen.queryByLabelText('原图 图1')).toBeNull();
expect(
screen.queryByRole('button', { name: '添加快速编辑参考图' }),
).toBeNull();
expect(screen.getByLabelText('参考图数量').textContent).toBe('1');
});
it('opens reference source menu and dispatches canvas picking or upload actions', () => {
it('does not expose quick edit reference pick actions', () => {
const requestUpload = vi.fn();
render(
<QuickEditPanelHarness
@@ -220,17 +215,14 @@ describe('ImageCanvasQuickEditPanelView', () => {
/>,
);
fireEvent.click(screen.getByRole('button', { name: '添加快速编辑参考图' }));
fireEvent.click(screen.getByRole('menuitem', { name: '从画布中选择' }));
expect(screen.getByLabelText('画布选择参考图').textContent).toBe('picking');
fireEvent.click(screen.getByRole('button', { name: '添加快速编辑参考图' }));
fireEvent.click(screen.getByRole('menuitem', { name: '上传图片' }));
expect(requestUpload).toHaveBeenCalledWith('quick-edit-reference');
expect(
screen.queryByRole('button', { name: '添加快速编辑参考图' }),
).toBeNull();
expect(requestUpload).not.toHaveBeenCalled();
expect(screen.getByLabelText('画布选择参考图').textContent).toBe('idle');
});
it('disables adding quick edit references after eight extra images', () => {
it('keeps quick edit reference controls hidden even with many references', () => {
render(
<QuickEditPanelHarness
initialPanel={{
@@ -251,12 +243,10 @@ describe('ImageCanvasQuickEditPanelView', () => {
);
expect(
(
screen.getByRole('button', {
name: '添加快速编辑参考图',
}) as HTMLButtonElement
).disabled,
).toBe(true);
screen.queryByRole('button', {
name: '添加快速编辑参考图',
}),
).toBeNull();
expect(screen.queryByLabelText('原图 图9')).toBeNull();
});
});
@@ -8,7 +8,10 @@ import {
} from 'react';
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
import { QUICK_EDIT_REFERENCE_LIMIT } from './ImageCanvasGenerationModel';
import {
IMAGE_MODEL_GPT_IMAGE_2,
QUICK_EDIT_REFERENCE_LIMIT,
} from './ImageCanvasGenerationModel';
import type {
CanvasLayer,
GenerateDialogState,
@@ -48,13 +51,14 @@ function resetFailedPanelStatus<
function createQuickEditDialog(
panel: QuickEditPanelState,
): GenerateDialogState {
const isQuickEdit = (panel.mode ?? 'quick-edit') === 'quick-edit';
return {
mode: panel.mode === 'redraw' ? 'generate' : 'quick-edit',
mode: isQuickEdit ? 'quick-edit' : 'generate',
prompt: panel.prompt,
status: panel.status,
sourceLayerId: panel.sourceLayerId,
generationReferences: panel.quickEditReferences,
imageModel: panel.model,
imageModel: isQuickEdit ? IMAGE_MODEL_GPT_IMAGE_2 : panel.model,
aspectRatio: panel.aspectRatio,
imageSize: panel.imageSize,
};
@@ -66,6 +70,7 @@ function syncPanelFromDialogUpdate(
) {
const normalizedPanel = resetFailedPanelStatus(panel);
const normalizedDialog = resetFailedPanelStatus(dialog);
const isQuickEdit = (normalizedPanel.mode ?? 'quick-edit') === 'quick-edit';
return {
...normalizedPanel,
size: panel.size,
@@ -73,7 +78,9 @@ function syncPanelFromDialogUpdate(
aspectRatio:
normalizedDialog.aspectRatio ?? normalizedPanel.aspectRatio ?? '1:1',
imageSize: normalizedDialog.imageSize ?? normalizedPanel.imageSize ?? '1K',
model: normalizedDialog.imageModel ?? normalizedPanel.model,
model: isQuickEdit
? IMAGE_MODEL_GPT_IMAGE_2
: (normalizedDialog.imageModel ?? normalizedPanel.model),
quickEditReferences: normalizedDialog.generationReferences,
status: normalizedDialog.status,
errorMessage: normalizedDialog.errorMessage,
@@ -137,12 +144,12 @@ export function ImageCanvasQuickEditPanelView({
onRememberImageModel={onRememberImageModel}
onSubmit={onSubmit}
dialogLabel={isRedraw ? '重绘图片' : '快速编辑图片'}
includeDimensions={true}
includeDimensions={isRedraw}
includeModel={true}
includeReferences={!isRedraw}
includeReferences={false}
promptLabel={isRedraw ? '重绘提示词' : '快速编辑提示词'}
promptPlaceholder={
isRedraw ? '把画面重绘成什么样?' : '想怎么编辑这张图?'
isRedraw ? '把画面重绘成什么样?' : '写下每个编号要怎么改'
}
optionLabelPrefix={isRedraw ? '重绘' : '快速编辑'}
dimensionRatioAriaLabelPrefix="比例"
@@ -153,7 +160,7 @@ export function ImageCanvasQuickEditPanelView({
referenceMenuLabel={isRedraw ? '' : '快速编辑参考图来源'}
referenceSlotTone="quick-edit"
referenceSlotDisabled={!canAddReferences}
showReferenceAddButton={!isRedraw}
showReferenceAddButton={false}
referenceLabelFormatter={(_, index) => `图${index + 1}`}
referenceAriaLabelFormatter={(_, index) => `图${index + 1}`}
referenceRemoveLabelFormatter={(_, index) => `删除图${index + 1}`}
@@ -162,9 +169,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="生成"
submitAriaLabel="生成"
submittingStatusLabel="生成中"
submitLabel={isRedraw ? '生成' : '修改'}
submitAriaLabel={isRedraw ? '生成' : '修改'}
submittingStatusLabel={isRedraw ? '生成中' : '修改中'}
renderEditorPortal={renderEditorPortal}
buildPortalMenuStyle={buildPortalMenuStyle}
/>
@@ -495,20 +495,23 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '填写快速编辑' }));
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
expect(screen.getByTestId('dialog').textContent).toBe(
'quick-edit:generating:closed:-:placeholder:-',
expect(screen.getByTestId('quick-edit').textContent).toBe(
'layer-source:generating:快速修图:-',
);
expect(screen.getByTestId('dialog').textContent).toBe('-');
await waitFor(() => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
prompt: '快速修图',
sourceImageSrc: 'data:image/png;base64,source.png',
size: '1024x768',
model: 'gemini-3.1-flash-image-preview',
model: 'gpt-image-2',
}),
);
});
expect(editEditorImageMock.mock.calls[0]?.[0]).not.toHaveProperty(
'canvasCompletion',
);
expect(generateEditorImageMock).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByTestId('layers').textContent).toContain(
@@ -522,6 +525,49 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(screen.getByTestId('fit-count').textContent).toBe('0');
});
it('clears quick edit panel after a queued edit reloads the project snapshot', async () => {
const applyProjectSnapshot = vi.fn();
const refreshTaskList = vi.fn();
const backendProject = {
projectId: 'editor-project-1',
title: '队列修图项目',
viewport: { x: 0, y: 0, scale: 1 },
layers: [
{
...createLayer(),
src: 'data:image/png;base64,queued-generated',
},
],
resources: [],
updatedAt: '2026-06-23T08:00:00.000Z',
};
loadEditorProjectMock.mockResolvedValueOnce(backendProject);
editEditorImageMock.mockResolvedValueOnce({
...createGenerated({ prompt: '快速修图' }),
queueState: createQueueState(),
});
render(
<SubmissionWorkflowHarness
projectId="editor-project-1"
applyProjectSnapshot={applyProjectSnapshot}
onQueuedGenerationTask={refreshTaskList}
/>,
);
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
fireEvent.click(screen.getByRole('button', { name: '填写快速编辑' }));
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
await waitFor(() => {
expect(loadEditorProjectMock).toHaveBeenCalledWith('editor-project-1');
});
expect(refreshTaskList).toHaveBeenCalledTimes(1);
expect(applyProjectSnapshot).toHaveBeenCalledWith(backendProject);
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
expect(screen.getByTestId('tool').textContent).toBe('select');
expect(screen.getByTestId('sidebar').textContent).toBe('layers');
});
it('submits quick-edit canvas generators with the source image reference', async () => {
editEditorImageMock.mockResolvedValueOnce(
createGenerated({ prompt: '把当前图改成雨天' }),
@@ -561,6 +607,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
size: '1024x768',
model: 'gpt-image-2',
sourceResourceId: 'resource-source',
targetLayerId: 'layer-source',
assetLabel: '源图 快速编辑',
}),
);
@@ -633,7 +680,7 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
);
});
it('submits quick edits with the numbered red selection image as the final source reference', async () => {
it('submits quick edits with the numbered red selection image as the source image', async () => {
editEditorImageMock.mockResolvedValueOnce(
createGenerated({
prompt: '对1号红色圈选框里的内容做以下修改:改成蓝色',
@@ -704,13 +751,99 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
sourceImageSrc: 'data:image/png;base64,marked-source',
referenceImageSrcs: ['data:image/png;base64,style.png'],
targetLayerId: 'layer-source',
}),
);
});
expect(editEditorImageMock.mock.calls[0]?.[0]).not.toHaveProperty(
'referenceImageSrcs',
);
expect(generateEditorImageMock).not.toHaveBeenCalled();
});
it('submits canvas quick edits with numbered selection marks', async () => {
editEditorImageMock.mockResolvedValueOnce(
createGenerated({
prompt: '对1号红色圈选框里的内容做以下修改:改成蓝色',
}),
);
render(
<SubmissionWorkflowHarness
initialDialog={{
id: 'generation-dialog-quick-edit-marked',
mode: 'quick-edit',
prompt: '对1号红色圈选框里的内容做以下修改:改成蓝色',
status: 'idle',
composerOpen: true,
sourceLayerId: 'layer-source',
imageModel: 'gpt-image-2',
aspectRatio: '4:3',
imageSize: '1K',
placeholder: {
x: 472,
y: 140,
width: 1024,
height: 768,
originalWidth: 1024,
originalHeight: 768,
},
}}
initialQuickEditSelectionState={{
sourceLayerId: 'layer-source',
tool: 'rect',
model: 'gemini-3.1-flash-image-preview',
marks: [
{
id: 'quick-edit-selection-mark-1',
tool: 'rect',
x: 24,
y: 36,
width: 160,
height: 120,
},
],
references: [],
draftMark: null,
status: 'idle',
}}
/>,
);
renderUiDesignAssetExtractionMarkedImageMock.mockResolvedValueOnce(
'data:image/png;base64,marked-source',
);
fireEvent.click(screen.getByRole('button', { name: '设置初始对话' }));
fireEvent.click(screen.getByRole('button', { name: '提交当前生成' }));
await waitFor(() => {
expect(renderUiDesignAssetExtractionMarkedImageMock).toHaveBeenCalledWith(
{
source: 'data:image/png;base64,source.png',
marks: [
{
id: 'quick-edit-selection-mark-1',
tool: 'rect',
x: 24,
y: 36,
width: 160,
height: 120,
},
],
showIndexLabels: true,
},
);
});
await waitFor(() => {
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
sourceImageSrc: 'data:image/png;base64,marked-source',
model: 'gpt-image-2',
targetLayerId: 'layer-source',
}),
);
});
});
it('keeps failed quick edit panels visible with the normalized prompt', async () => {
const refreshWalletBalance = vi.fn();
editEditorImageMock.mockRejectedValueOnce(new Error('修图失败'));
@@ -864,7 +997,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
resolution: '480p',
mode: 'std',
sound: 'off',
referenceImageSrcs: ['data:image/png;base64,style.png'],
referenceVideoSrcs: [
'generated-character-drafts/editor/videos/source.mp4',
],
@@ -873,6 +1005,9 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
}),
);
});
expect(generateEditorVideoMock.mock.calls[0]?.[0]).not.toHaveProperty(
'referenceImageSrcs',
);
expect(generateEditorImageMock).not.toHaveBeenCalled();
expect(generateEditorCharacterAnimationMock).not.toHaveBeenCalled();
await waitFor(() => {
@@ -888,24 +1023,13 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
});
});
it('submits character action quick edits through seedance fast character animation generation', async () => {
generateEditorCharacterAnimationMock.mockResolvedValueOnce({
taskId: 'action-quick-edit-task',
model: 'seedance2.0-fast',
prompt: '让图1跳得更高',
previewVideoPath: '/generated-character-drafts/editor/action-preview.mp4',
frames: [
{
frameIndex: 1,
imageSrc: 'data:image/png;base64,action-frame-edited',
width: 1024,
height: 1024,
},
],
frameCount: 32,
durationSeconds: 4,
fps: 8,
});
it('submits character action quick edits through image edits', async () => {
editEditorImageMock.mockResolvedValueOnce(
createGenerated({
prompt: '让当前图跳得更高',
imageSrc: 'data:image/png;base64,action-frame-edited',
}),
);
render(
<SubmissionWorkflowHarness
initialLayers={[
@@ -945,24 +1069,22 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
await waitFor(() => {
expect(generateEditorCharacterAnimationMock).toHaveBeenCalledWith(
expect(editEditorImageMock).toHaveBeenCalledWith(
expect.objectContaining({
sourceLayerId: 'layer-source',
sourceImageSrc: 'generated-character-drafts/editor/actions/frame.png',
promptText: '让当前图跳得更高',
resolution: '480p',
ratio: 'same',
frameCount: 32,
durationSeconds: 4,
model: 'seedance2.0-fast',
prompt: '让当前图跳得更高',
sourceImageSrc: 'data:image/png;base64,frame.png',
model: 'gpt-image-2',
assetLabel: '角色动作 快速编辑',
targetLayerId: 'layer-source',
}),
);
});
expect(generateEditorCharacterAnimationMock).not.toHaveBeenCalled();
expect(generateEditorImageMock).not.toHaveBeenCalled();
expect(generateEditorVideoMock).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.getByTestId('layers').textContent).toContain(
'layer-character-animation-1:角色动作 快速编辑:resource-source:character-animation:image-sequence:data:image/png;base64,action-frame-edited:data:image/png;base64,action-frame-edited:4',
'layer-source:角色动作:resource-source:character-animation:image-sequence:data:image/png;base64,action-frame-edited',
);
});
});
@@ -62,6 +62,7 @@ import {
DEFAULT_VIDEO_MODEL,
ICON_FRAME_DISPLAY_SIZE,
ICON_FRAME_ORIGINAL_SIZE,
IMAGE_MODEL_GPT_IMAGE_2,
inferEditorImageAspectRatio,
inferEditorImageSizeLabel,
isCanvasGenerationDialog,
@@ -281,10 +282,6 @@ function isVideoQuickEditSource(layer: CanvasLayer) {
return layer.mediaType === 'video' || layer.assetKind === 'video';
}
function isCharacterAnimationQuickEditSource(layer: CanvasLayer) {
return layer.assetKind === 'character-animation';
}
export function useImageCanvasGenerationSubmissionWorkflow({
layers,
canvasSize,
@@ -953,7 +950,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
const panelMode = quickEditPanel.mode ?? 'quick-edit';
const quickEditReferences =
panelMode === 'redraw' ? [] : (quickEditPanel.quickEditReferences ?? []);
panelMode === 'redraw' || panelMode === 'quick-edit'
? []
: (quickEditPanel.quickEditReferences ?? []);
const quickEditAspectRatio =
quickEditPanel.aspectRatio ??
inferEditorImageAspectRatio(
@@ -977,7 +976,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({
(panelMode === 'redraw' ? '重绘图片' : '快速编辑图片');
const normalizedPrompt = panelMode === 'redraw' ? basePrompt : basePrompt;
let quickEditDialogId: string | undefined;
if (panelMode === 'quick-edit') {
const shouldUseCanvasPlaceholder =
panelMode === 'quick-edit' && isVideoQuickEditSource(quickEditSourceLayer);
if (shouldUseCanvasPlaceholder) {
const quickEditDraft = createQuickEditGenerationDialogDraft({
sourceLayer: quickEditSourceLayer,
prompt: normalizedPrompt,
@@ -1002,7 +1003,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
: quickEditDraft.placeholder;
quickEditDialogId = openCanvasGenerationDialog({
...quickEditDraft,
// 中文注释:快速编辑生成结果复用新建图片的避让落点,避免覆盖已有素材。
// 中文注释:视频快速编辑仍落到独立视频结果占位,图片快速编辑直接替换源图。
placeholder: quickEditPlacement,
});
setQuickEditPanel(null);
@@ -1049,72 +1050,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({
originalWidth: quickEditOutputSize.width,
originalHeight: quickEditOutputSize.height,
});
if (
panelMode === 'quick-edit' &&
isCharacterAnimationQuickEditSource(quickEditSourceLayer)
) {
const animationResolution = '480p' as const;
const animationDurationSeconds = 4 as const;
const animationFrameCount = 32 as const;
const generationInputs = buildCharacterAnimationGenerationInputs(
normalizedPrompt,
quickEditSourceLayer,
);
const generated = await runEditorGenerationWithWalletRefresh(
generateEditorCharacterAnimation({
sourceLayerId: quickEditSourceLayer.id,
sourceImageSrc:
resolveCharacterAnimationSourceImageSrc(quickEditSourceLayer),
sourceWidth: quickEditSourceLayer.originalWidth,
sourceHeight: quickEditSourceLayer.originalHeight,
promptText: normalizedPrompt,
resolution: animationResolution,
ratio: 'same',
frameCount: animationFrameCount,
durationSeconds: animationDurationSeconds,
model: CHARACTER_ANIMATION_MODEL,
projectId,
generationInputs,
sourceResourceId: quickEditSourceLayer.resourceId,
...(projectId && quickEditCanvasCompletionPlaceholder
? {
canvasCompletion: {
dialogId: quickEditDialogId,
title: `${quickEditSourceLayer.title} 快速编辑`,
placeholder: quickEditCanvasCompletionPlaceholder,
},
}
: {}),
}),
onWalletBalanceMayHaveChanged,
);
if (
await applyQueuedEditorGenerationProject(
generated,
projectId,
applyProjectSnapshot,
onQueuedGenerationTask,
onWalletBalanceMayHaveChanged,
)
) {
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
return;
}
addCharacterAnimationResultLayer(
generated,
`${quickEditSourceLayer.title} 快速编辑`,
generationInputs,
quickEditFrame,
quickEditDialogId,
{
sourceLayer: quickEditSourceLayer,
},
);
return;
}
if (
panelMode === 'quick-edit' &&
isVideoQuickEditSource(quickEditSourceLayer)
@@ -1229,7 +1164,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
prompt: normalizedPrompt,
sourceImageSrc: sourceReferenceImageSrc,
size: quickEditSize,
model: normalizeEditorImageModel(quickEditPanel.model),
model: IMAGE_MODEL_GPT_IMAGE_2,
...(extraReferenceImageSrcs.length
? { referenceImageSrcs: extraReferenceImageSrcs }
: {}),
@@ -1239,6 +1174,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
assetFolderId,
assetLabel: `${quickEditSourceLayer.title} 快速编辑`,
sourceResourceId: quickEditSourceLayer.resourceId,
targetLayerId: quickEditSourceLayer.id,
}),
onWalletBalanceMayHaveChanged,
)
@@ -1281,10 +1217,20 @@ export function useImageCanvasGenerationSubmissionWorkflow({
onWalletBalanceMayHaveChanged,
)
) {
if (panelMode === 'quick-edit') {
setQuickEditPanel(null);
setActiveTool('select');
setActiveSidebarPanel('layers');
}
return;
}
if (generated.project && applyProjectSnapshot) {
applyProjectSnapshot(generated.project);
if (panelMode === 'quick-edit') {
setQuickEditPanel(null);
setActiveTool('select');
setActiveSidebarPanel('layers');
}
return;
}
if (panelMode === 'quick-edit') {
@@ -1448,10 +1394,22 @@ export function useImageCanvasGenerationSubmissionWorkflow({
generationInputs: submissionPlan.generationInputs,
});
} else if (submissionPlan.kind === 'quick-edit') {
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
let referenceImageSrc = await resolveEditorImageReferenceDataUrl(
submissionPlan.sourceLayer.objectKey?.trim() ||
submissionPlan.sourceLayer.src,
);
const quickEditSelectionMarks =
quickEditSelectionState?.sourceLayerId ===
submissionPlan.sourceLayer.id
? quickEditSelectionState.marks
: [];
if (quickEditSelectionMarks.length > 0) {
referenceImageSrc = await renderUiDesignAssetExtractionMarkedImage({
source: referenceImageSrc,
marks: quickEditSelectionMarks,
showIndexLabels: true,
});
}
const generated = await runEditorGenerationWithWalletRefresh(
editEditorImage({
prompt: submissionPlan.normalizedPrompt,
@@ -1463,6 +1421,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
assetFolderId,
assetLabel: submissionPlan.result.title,
sourceResourceId: submissionPlan.sourceLayer.resourceId,
targetLayerId: submissionPlan.sourceLayer.id,
}),
onWalletBalanceMayHaveChanged,
);
@@ -1701,6 +1660,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
getGeneratingDialogPlaceholder,
layerCounterRef,
layers,
quickEditSelectionState,
rememberImageModel,
projectId,
assetFolderId,
@@ -231,6 +231,11 @@ function GenerationWorkflowHarness({
? `${workflow.quickEditPanel.sourceLayerId}:${workflow.quickEditPanel.status}:${workflow.quickEditPanel.prompt || '-'}`
: '-'}
</span>
<span data-testid="quick-edit-image-options">
{workflow.quickEditPanel
? `${workflow.quickEditPanel.mode ?? 'quick-edit'}:${workflow.quickEditPanel.model}:${workflow.quickEditPanel.aspectRatio ?? '-'}:${workflow.quickEditPanel.imageSize ?? '-'}`
: '-'}
</span>
<span data-testid="crop-expand">
{workflow.cropExpandPanel
? `${workflow.cropExpandPanel.sourceLayerId}:${workflow.cropExpandPanel.ratio}:${workflow.cropExpandPanel.frame.x}:${workflow.cropExpandPanel.frame.y}:${workflow.cropExpandPanel.frame.width}:${workflow.cropExpandPanel.frame.height}:${workflow.cropExpandPanel.status}`
@@ -833,18 +838,22 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
});
it('opens the source generator for generated character quick edit', () => {
it('opens quick edit for generated character images', () => {
render(<GenerationWorkflowHarness />);
fireEvent.click(screen.getByRole('button', { name: '快速编辑角色图' }));
expect(screen.getByTestId('tool').textContent).toBe('character');
expect(screen.getByTestId('selected').textContent).toBe('-');
expect(screen.getByTestId('dialog').textContent).toBe(
'character:idle:open:-:placeholder',
expect(screen.getByTestId('tool').textContent).toBe('generate');
expect(screen.getByTestId('selected').textContent).toBe(
'layer-character-generated',
);
expect(screen.getByTestId('dialog').textContent).toBe('-');
expect(screen.getByTestId('quick-edit').textContent).toBe(
'layer-character-generated:idle:-',
);
expect(screen.getByTestId('quick-edit-selection').textContent).toBe(
'layer-character-generated:none:0',
);
expect(screen.getByTestId('dialog-prompt').textContent).toBe('-');
expect(screen.getByTestId('quick-edit-selection').textContent).toBe('-');
});
it('opens the source generator for generated publication redraw', () => {
@@ -864,7 +873,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
});
it('opens the source generator for plain generated images with a source dialog', () => {
it('opens quick edit for plain generated images without inheriting hidden source dimensions', () => {
render(<GenerationWorkflowHarness />);
fireEvent.click(
@@ -873,23 +882,27 @@ describe('useImageCanvasGenerationWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '快速编辑来源普通图' }));
expect(screen.getByTestId('tool').textContent).toBe('generate');
expect(screen.getByTestId('dialog').textContent).toBe(
'generate:idle:open:-:placeholder',
expect(screen.getByTestId('dialog').textContent).toBe('-');
expect(screen.getByTestId('quick-edit').textContent).toBe(
'layer-source:idle:-',
);
expect(screen.getByTestId('dialog-image-options').textContent).toBe(
'generate:gpt-image-2:2:3:2K',
expect(screen.getByTestId('quick-edit-image-options').textContent).toBe(
'quick-edit:gpt-image-2:4:3:1K',
);
expect(screen.getByTestId('quick-edit-selection').textContent).toBe(
'layer-source:none:0',
);
expect(screen.getByTestId('quick-edit-selection').textContent).toBe('-');
});
it('keeps plain images without a source generator on quick edit generator', () => {
it('keeps plain images without a source generator on the quick edit panel', () => {
render(<GenerationWorkflowHarness />);
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
expect(screen.getByTestId('tool').textContent).toBe('generate');
expect(screen.getByTestId('dialog').textContent).toBe(
'quick-edit:idle:open:-:placeholder',
expect(screen.getByTestId('dialog').textContent).toBe('-');
expect(screen.getByTestId('quick-edit').textContent).toBe(
'layer-source:idle:-',
);
expect(screen.getByTestId('quick-edit-selection').textContent).toBe(
'layer-source:none:0',
@@ -994,12 +1007,12 @@ describe('useImageCanvasGenerationWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '打开生成' }));
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
expect(screen.getByTestId('dialog').textContent).toBe(
'quick-edit:idle:open:-:placeholder',
expect(screen.getByTestId('dialog').textContent).toBe('-');
expect(screen.getByTestId('quick-edit').textContent).toBe(
'layer-source:idle:-',
);
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
expect(screen.getByTestId('generation-dialogs').textContent).toBe(
'generation-dialog-1:generate:closed|generation-dialog-2:quick-edit:open',
'generation-dialog-1:generate:closed',
);
});
@@ -1019,7 +1032,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
});
it('keeps source image model and dimensions when opening quick edit', () => {
it('keeps source image model and infers dimensions from the current image when opening quick edit', () => {
render(
<GenerationWorkflowHarness
initialLayers={[
@@ -1037,11 +1050,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
);
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
expect(screen.getByTestId('dialog').textContent).toBe(
'generate:idle:open:-:placeholder',
);
expect(screen.getByTestId('dialog-image-options').textContent).toBe(
'generate:gpt-image-2:2:3:2K',
expect(screen.getByTestId('dialog').textContent).toBe('-');
expect(screen.getByTestId('quick-edit-image-options').textContent).toBe(
'quick-edit:gpt-image-2:3:2:2K',
);
});
@@ -1483,7 +1494,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
expect(screen.getByTestId('quick-edit').textContent).toBe(
'layer-source:idle:-',
);
expect(screen.getByTestId('quick-edit-selection').textContent).toBe(
'layer-source:none:0',
);
@@ -1526,7 +1539,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
expect(screen.getByTestId('quick-edit-selection').textContent).toBe(
'layer-source:rect:1',
);
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
expect(screen.getByTestId('quick-edit').textContent).toBe(
'layer-source:idle:对1号红色圈选框里的内容做以下修改:',
);
fireEvent.click(
screen.getByRole('button', { name: '选择快速编辑椭圆框选' }),
@@ -1536,7 +1551,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
expect(screen.getByTestId('quick-edit-selection').textContent).toBe(
'layer-source:ellipse:2',
);
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
expect(screen.getByTestId('quick-edit').textContent).toBe(
'layer-source:idle:对1号红色圈选框里的内容做以下修改:\n对2号红色圈选框里的内容做以下修改:',
);
});
it('submits quick edits and updates the source layer directly', async () => {
@@ -1546,8 +1563,8 @@ describe('useImageCanvasGenerationWorkflow', () => {
render(<GenerationWorkflowHarness />);
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
fireEvent.click(screen.getByRole('button', { name: '填写快速编辑提示词' }));
fireEvent.click(screen.getByRole('button', { name: '提交生成' }));
fireEvent.click(screen.getByRole('button', { name: '填写快速编辑' }));
fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' }));
await waitFor(() => {
expect(editEditorImageMock).toHaveBeenCalledWith(
@@ -1558,7 +1575,14 @@ describe('useImageCanvasGenerationWorkflow', () => {
sourceResourceId: 'resource-source',
generationInputs: {
fields: [{ title: '快速编辑提示词', value: '快速修图' }],
references: [],
references: [
{
title: '原图',
label: '源图',
refType: 'project-resource',
refId: 'resource-source',
},
],
},
}),
);
@@ -1755,7 +1779,9 @@ describe('useImageCanvasGenerationWorkflow', () => {
fireEvent.click(screen.getByRole('button', { name: '打开快速编辑' }));
await waitFor(() => {
expect(screen.getByTestId('quick-edit').textContent).toBe('-');
expect(screen.getByTestId('quick-edit').textContent).toBe(
'layer-source:idle:-',
);
});
fireEvent.click(screen.getByRole('button', { name: '打开修改状态' }));
fireEvent.click(screen.getByRole('button', { name: '打开UI素材框选' }));
@@ -1024,8 +1024,6 @@ export function useImageCanvasGenerationWorkflow({
);
return createQuickEditPanelDraft(sourceLayer, {
imageModel: sourceDialog?.imageModel,
aspectRatio: sourceDialog?.aspectRatio,
imageSize: sourceDialog?.imageSize,
});
},
[canvasGenerationDialogs],
@@ -1038,48 +1036,11 @@ export function useImageCanvasGenerationWorkflow({
setUiAssetExtractionState(null);
setCropExpandPanel(null);
setCharacterAnimationPanel(null);
const sourceDialog = findSourceGenerationDialog(
canvasGenerationDialogs,
sourceLayer,
);
const sameSourceDraft = createSameSourceGenerationDialogDraft({
sourceLayer,
canvasSize,
viewport,
sourceDialog,
mode: 'quick-edit',
sourceAnimationLayer: findSourceAnimationLayer(layers, sourceLayer),
});
if (sameSourceDraft) {
openPlacedCanvasGenerationDialog(sameSourceDraft);
setQuickEditPanel(null);
setQuickEditSelectionState(null);
selectSingleLayer(null);
setActiveTool(getCanvasToolForGenerationMode(sameSourceDraft.mode));
return;
}
const quickEditDraft = createQuickEditModeDraft(sourceLayer);
const quickEditSize = resolveEditorImageGenerationPixelSize({
model: quickEditDraft.model,
aspectRatio: quickEditDraft.aspectRatio,
imageSize: quickEditDraft.imageSize,
});
openPlacedCanvasGenerationDialog({
...createQuickEditGenerationDialogDraft({
sourceLayer,
prompt: quickEditDraft.prompt,
references: quickEditDraft.quickEditReferences ?? [],
model: quickEditDraft.model,
aspectRatio: quickEditDraft.aspectRatio,
imageSize: quickEditDraft.imageSize,
frame: {
width: quickEditSize.width,
height: quickEditSize.height,
},
}),
composerOpen: true,
});
setQuickEditPanel(null);
archiveActiveCanvasGenerationDialog();
setGenerateDialog(null);
setQuickEditPanel(quickEditDraft);
selectSingleLayer(sourceLayer.id);
const nextSelectionState = createUiAssetExtractionState(sourceLayer.id, {
initialTool: null,
model: DEFAULT_IMAGE_MODEL,
@@ -1087,22 +1048,20 @@ export function useImageCanvasGenerationWorkflow({
quickEditSelectionStateRef.current = nextSelectionState;
setQuickEditSelectionState(nextSelectionState);
quickEditSelectionMarkCounterRef.current = 0;
selectSingleLayer(sourceLayer.id);
setViewport(resolveQuickEditFocusViewport({ sourceLayer, canvasSize }));
setActiveTool('generate');
},
[
archiveActiveCanvasGenerationDialog,
canvasSize,
createQuickEditModeDraft,
canvasGenerationDialogs,
layers,
openPlacedCanvasGenerationDialog,
selectSingleLayer,
setActiveTool,
setGenerateDialog,
setImageContextMenu,
setMetadataLayer,
setQuickEditPanel,
setViewport,
viewport,
],
);
@@ -2012,6 +1971,23 @@ export function useImageCanvasGenerationWorkflow({
draftMark: null,
marks: [...currentSelectionState.marks, normalizedMark],
}));
setQuickEditPanel((currentPanel) =>
currentPanel?.sourceLayerId === currentSelectionState.sourceLayerId
? {
...currentPanel,
status:
currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
errorMessage:
currentPanel.status === 'failed'
? undefined
: currentPanel.errorMessage,
prompt: appendQuickEditSelectionPrompt(
currentPanel.prompt,
nextMarkNumber,
),
}
: currentPanel,
);
setGenerateDialog((currentDialog) =>
currentDialog?.mode === 'quick-edit' &&
currentDialog.sourceLayerId === currentSelectionState.sourceLayerId
@@ -2031,7 +2007,7 @@ export function useImageCanvasGenerationWorkflow({
}
: currentDialog,
);
}, [setGenerateDialog, updateQuickEditSelectionState]);
}, [setGenerateDialog, setQuickEditPanel, updateQuickEditSelectionState]);
useEffect(() => {
if (!quickEditSelectionState) {
@@ -1398,6 +1398,7 @@ describe('editorProjectClient', () => {
assetFolderId: 'project',
assetLabel: '角色形象 修改结果',
sourceResourceId: 'resource-character-1',
targetLayerId: 'layer-character-1',
generationInputs: {
fields: [{ title: '修改提示词', value: '把画面改成黄昏光线' }],
references: [],
@@ -1425,6 +1426,7 @@ describe('editorProjectClient', () => {
assetFolderId: 'project',
assetLabel: '角色形象 修改结果',
sourceResourceId: 'resource-character-1',
targetLayerId: 'layer-character-1',
}),
}),
'修改图片失败',
@@ -196,6 +196,7 @@ export type EditorImageEditInput = {
assetFolderId?: string | null;
assetLabel?: string | null;
sourceResourceId?: string | null;
targetLayerId?: string | null;
canvasCompletion?: EditorCanvasGenerationCompletionInput | null;
};
@@ -910,6 +911,7 @@ export async function editEditorImage(input: EditorImageEditInput) {
...(input.sourceResourceId
? { sourceResourceId: input.sourceResourceId }
: {}),
...(input.targetLayerId ? { targetLayerId: input.targetLayerId } : {}),
...(input.canvasCompletion
? { canvasCompletion: input.canvasCompletion }
: {}),