diff --git a/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx b/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx index 2e46075a9..157951b83 100644 --- a/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx @@ -959,9 +959,9 @@ describe('ImageCanvasEditorView generation integration', () => { clientY: 120, }); fireEvent.click(screen.getByRole('button', { name: '快速编辑' })); - expect(screen.getByRole('dialog', { name: '快速编辑图片' })).toBeTruthy(); + expect(screen.getByRole('dialog', { name: '改造图片' })).toBeTruthy(); expect( - screen.queryByRole('button', { name: '关闭快速编辑图片' }), + screen.queryByRole('button', { name: '关闭改造图片' }), ).toBeNull(); fireEvent.pointerDown(viewport, { @@ -971,7 +971,7 @@ describe('ImageCanvasEditorView generation integration', () => { clientY: 180, }); - expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull(); + expect(screen.queryByRole('dialog', { name: '改造图片' })).toBeNull(); fireEvent.pointerDown(sourceLayer, { button: 0, @@ -2785,7 +2785,7 @@ describe('ImageCanvasEditorView generation integration', () => { expect(screen.getByText('角色')).toBeTruthy(); expect(screen.getByRole('button', { name: '生成动画' })).toBeTruthy(); expect(screen.queryByRole('dialog', { name: '重绘图片' })).toBeNull(); - expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull(); + expect(screen.queryByRole('dialog', { name: '改造图片' })).toBeNull(); expect( screen.queryByRole('dialog', { name: '角色动画生成面板' }), ).toBeNull(); @@ -2796,7 +2796,7 @@ describe('ImageCanvasEditorView generation integration', () => { expect(screen.getByRole('menuitem', { name: '生成动画' })).toBeTruthy(); expect(screen.queryByRole('dialog', { name: '重绘图片' })).toBeNull(); - expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull(); + expect(screen.queryByRole('dialog', { name: '改造图片' })).toBeNull(); fireEvent.click(screen.getByRole('button', { name: '生成动画' })); expect( @@ -3242,7 +3242,7 @@ describe('ImageCanvasEditorView generation integration', () => { }); fireEvent.click(screen.getByRole('button', { name: '快速编辑' })); - const quickPanel = screen.getByRole('dialog', { name: '快速编辑图片' }); + const quickPanel = screen.getByRole('dialog', { name: '改造图片' }); expect(quickPanel.className).toContain( 'image-canvas-editor__quick-edit-panel', ); @@ -3284,7 +3284,7 @@ describe('ImageCanvasEditorView generation integration', () => { ); const updatedQuickPanel = screen.getByRole('dialog', { - name: '快速编辑图片', + name: '改造图片', }); expect(within(updatedQuickPanel).getByText('星空参考')).toBeTruthy(); expect(within(updatedQuickPanel).queryByLabelText('原图 图2')).toBeNull(); @@ -3411,7 +3411,7 @@ describe('ImageCanvasEditorView generation integration', () => { ).toBeTruthy(); fireEvent.click(within(menu).getByRole('menuitem', { name: '快速编辑' })); - const panel = screen.getByRole('dialog', { name: '快速编辑图片' }); + const panel = screen.getByRole('dialog', { name: '改造图片' }); expect(within(panel).queryByText('右键图片')).toBeNull(); fireEvent.change(within(panel).getByLabelText('快速编辑提示词'), { target: { value: '换成夜晚' }, @@ -3502,7 +3502,7 @@ describe('ImageCanvasEditorView generation integration', () => { }); fireEvent.click(screen.getByRole('button', { name: '快速编辑' })); - const quickPanel = screen.getByRole('dialog', { name: '快速编辑图片' }); + const quickPanel = screen.getByRole('dialog', { name: '改造图片' }); fireEvent.change(within(quickPanel).getByLabelText('快速编辑提示词'), { target: { value: '改成陶泥风格' }, }); @@ -3753,13 +3753,13 @@ describe('ImageCanvasEditorView generation integration', () => { clientY: 180, }); fireEvent.click(screen.getByRole('button', { name: '快速编辑' })); - const quickPanel = screen.getByRole('dialog', { name: '快速编辑图片' }); + const quickPanel = screen.getByRole('dialog', { name: '改造图片' }); fireEvent.change(within(quickPanel).getByLabelText('快速编辑提示词'), { target: { value: '加一层暖光' }, }); fireEvent.click(within(quickPanel).getByRole('button', { name: '生成' })); - expect(screen.queryByRole('dialog', { name: '快速编辑图片' })).toBeNull(); + expect(screen.queryByRole('dialog', { name: '改造图片' })).toBeNull(); expect(screen.getByAltText('画布图片:快速编辑源图')).toBeTruthy(); expect(sourceLayer.className).not.toContain( 'image-canvas-editor__layer--generating', diff --git a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx index 5b3112d99..c00ac9ef6 100644 --- a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx +++ b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx @@ -117,6 +117,36 @@ function renderComposer( } describe('ImageCanvasGenerationComposerView', () => { + it('让快速编辑复用普通图片生成面板结构', () => { + renderComposer({ + mode: 'quick-edit', + prompt: '', + status: 'idle', + composerOpen: true, + sourceLayerId: 'layer-source', + generationReferences: [], + imageModel: 'gpt-image-2', + aspectRatio: '1:1', + imageSize: '1K', + }); + + const panel = screen.getByRole('dialog', { name: '改造图片' }); + expect(panel.className).toContain( + 'image-canvas-editor__generation-composer', + ); + expect(panel.className).toContain( + 'image-canvas-editor__generation-composer--image', + ); + expect(panel.className).not.toContain( + 'image-canvas-editor__quick-edit-panel', + ); + expect(within(panel).getByRole('textbox', { name: '改造提示词' })) + .toBeTruthy(); + expect( + panel.querySelector('.image-canvas-editor__generation-composer-footer'), + ).toBeTruthy(); + }); + it('让生成UI设计图面板复用普通图片生成面板的纵向结构', () => { const setGenerateDialog = vi.fn(); renderComposer({ diff --git a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts index e08aad069..4f6164920 100644 --- a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts @@ -559,6 +559,41 @@ export function createAudioRedrawGenerationDialogDraft( }; } + +export function createVideoRedrawGenerationDialogDraft( + sourceLayer: CanvasLayer, +): Omit | null { + if (sourceLayer.mediaType !== 'video') { + return null; + } + const placeholderSize = resolveEditorVideoGenerationPixelSize({ + aspectRatio: DEFAULT_VIDEO_ASPECT_RATIO, + resolution: '480p', + }); + return { + mode: 'video', + sourceLayerId: sourceLayer.id, + prompt: resolveUserGenerationPromptSnapshot(sourceLayer), + status: 'idle', + composerOpen: true, + generationReferences: [createCanvasLayerReference(sourceLayer)], + videoModel: DEFAULT_VIDEO_MODEL, + videoAspectRatio: DEFAULT_VIDEO_ASPECT_RATIO, + videoResolution: '480p', + videoDurationSeconds: DEFAULT_VIDEO_DURATION_SECONDS, + videoMode: 'std', + videoSound: DEFAULT_VIDEO_SOUND, + videoWebSearchEnabled: DEFAULT_VIDEO_WEB_SEARCH_ENABLED, + placeholder: { + x: sourceLayer.x + sourceLayer.width + 32, + y: sourceLayer.y, + width: placeholderSize.width, + height: placeholderSize.height, + originalWidth: placeholderSize.width, + originalHeight: placeholderSize.height, + }, + }; +} export function createUiDesignGenerationDialogDraft({ canvasSize, viewport, @@ -784,7 +819,7 @@ export function appendGenerationReference( if (dialog?.mode === 'video' && !isSeedanceVideoModel(dialog.videoModel)) { return dialog; } - if (dialog?.mode === 'generate') { + if (dialog?.mode === 'generate' || dialog?.mode === 'quick-edit') { if (getReferenceMediaType(layer) !== 'image') { return dialog; } diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts index 5836af849..6ae715931 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts @@ -36,7 +36,6 @@ import { appendCharacterReference, appendGenerationReference, appendPublicationReference, - appendQuickEditReference, assignCharacterSpecReference, assignIconSpecReference, assignUiDesignSpecReference, @@ -55,6 +54,7 @@ import { createSoundEffectGenerationDialogDraft, createSpecDialogDraft, createUiDesignGenerationDialogDraft, + createVideoRedrawGenerationDialogDraft, createVideoGenerationDialogDraft, hideGeneratedLayerComposerAfterBlur, isCharacterSpecReferenceLayer, @@ -900,11 +900,30 @@ export function useImageCanvasGenerationWorkflow({ setImageContextMenu(null); setMetadataLayer(null); setUiAssetExtractionState(null); - archiveActiveCanvasGenerationDialog(); - setGenerateDialog(null); setCropExpandPanel(null); setCharacterAnimationPanel(null); - setQuickEditPanel(createQuickEditPanelDraft(sourceLayer)); + const quickEditDraft = createQuickEditPanelDraft(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); const nextSelectionState = createUiAssetExtractionState(sourceLayer.id, { initialTool: null, }); @@ -917,10 +936,9 @@ export function useImageCanvasGenerationWorkflow({ }, [ canvasSize, + openPlacedCanvasGenerationDialog, selectSingleLayer, - archiveActiveCanvasGenerationDialog, setActiveTool, - setGenerateDialog, setImageContextMenu, setMetadataLayer, setViewport, @@ -973,6 +991,17 @@ export function useImageCanvasGenerationWorkflow({ setActiveTool('music'); return; } + if (sourceLayer.mediaType === 'video') { + const videoDraft = createVideoRedrawGenerationDialogDraft(sourceLayer); + if (!videoDraft) { + return; + } + setQuickEditPanel(null); + selectSingleLayer(sourceLayer.id); + openCanvasGenerationDialog(videoDraft); + setActiveTool('video'); + return; + } if ( sourceLayer.mediaType === undefined || sourceLayer.mediaType === 'image' @@ -1002,17 +1031,12 @@ export function useImageCanvasGenerationWorkflow({ setActiveTool('generate'); return; } - setGenerateDialog(null); - setQuickEditPanel(createRedrawPanelDraft(sourceLayer)); - selectSingleLayer(sourceLayer.id); - setActiveTool('generate'); }, [ openCanvasGenerationDialog, openPlacedCanvasGenerationDialog, selectSingleLayer, setActiveTool, - setGenerateDialog, setImageContextMenu, setMetadataLayer, ], @@ -1275,13 +1299,14 @@ export function useImageCanvasGenerationWorkflow({ const pickQuickEditReferenceFromLayer = useCallback( (layer: CanvasLayer) => { - setQuickEditPanel((currentPanel) => - appendQuickEditReference(currentPanel, layer), + setGenerateDialog((currentDialog) => + appendGenerationReference(currentDialog, layer), ); setIsPickingQuickEditReferenceFromCanvas(false); + setIsGenerationReferenceMenuOpen(false); setImageContextMenu(null); }, - [setImageContextMenu], + [setGenerateDialog, setImageContextMenu], ); const pickCharacterReferenceFromLayer = useCallback( @@ -1626,31 +1651,39 @@ export function useImageCanvasGenerationWorkflow({ draftMark: null, marks: [...currentSelectionState.marks, normalizedMark], })); - setQuickEditPanel((currentPanel) => - currentPanel + setGenerateDialog((currentDialog) => + currentDialog?.mode === 'quick-edit' && + currentDialog.sourceLayerId === currentSelectionState.sourceLayerId ? { - ...currentPanel, + ...currentDialog, status: - currentPanel.status === 'failed' ? 'idle' : currentPanel.status, + currentDialog.status === 'failed' ? 'idle' : currentDialog.status, errorMessage: - currentPanel.status === 'failed' + currentDialog.status === 'failed' ? undefined - : currentPanel.errorMessage, + : currentDialog.errorMessage, prompt: appendQuickEditSelectionPrompt( - currentPanel.prompt, + currentDialog.prompt, nextMarkNumber, ), + composerOpen: true, } - : currentPanel, + : currentDialog, ); - }, [updateQuickEditSelectionState]); + }, [setGenerateDialog, updateQuickEditSelectionState]); useEffect(() => { - if (!quickEditPanel && quickEditSelectionState) { + if (!quickEditSelectionState) { + return; + } + const hasActiveQuickEditDialog = + generateDialog?.mode === 'quick-edit' && + generateDialog.sourceLayerId === quickEditSelectionState.sourceLayerId; + if (!hasActiveQuickEditDialog && !quickEditPanel) { quickEditSelectionStateRef.current = null; setQuickEditSelectionState(null); } - }, [quickEditPanel, quickEditSelectionState]); + }, [generateDialog, quickEditPanel, quickEditSelectionState]); const cancelUiAssetExtraction = useCallback(() => { setUiAssetExtractionState(null);