diff --git a/src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx b/src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx index c4484410c..82aa5f7ee 100644 --- a/src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx +++ b/src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx @@ -29,12 +29,10 @@ function BasicGenerationHarness({ initialDialog = createDialog(), onRequestUpload = vi.fn(), onSubmit = vi.fn(), - onClose = vi.fn(), }: { initialDialog?: GenerateDialogState; onRequestUpload?: (target: UploadTarget) => void; onSubmit?: (dialog: GenerateDialogState) => void; - onClose?: () => void; }) { const [dialog, setDialog] = useState( initialDialog, @@ -58,7 +56,6 @@ function BasicGenerationHarness({ onRequestUpload={onRequestUpload} onToggleReferenceMenu={() => setIsReferenceMenuOpen((open) => !open)} onSubmit={onSubmit} - onClose={onClose} /> {dialog.prompt} {dialog.aspectRatio} @@ -206,12 +203,11 @@ describe('ImageCanvasBasicGenerationComposerView', () => { ).toBeNull(); }); - it('closes through its interface', () => { - const closeComposer = vi.fn(); - render(); + it('does not render a standalone close button', () => { + render(); - fireEvent.click(screen.getByRole('button', { name: '关闭生成图片' })); - - expect(closeComposer).toHaveBeenCalledTimes(1); + expect( + screen.queryByRole('button', { name: '关闭生成图片' }), + ).toBeNull(); }); }); diff --git a/src/components/image-editor/ImageCanvasBasicGenerationComposerView.tsx b/src/components/image-editor/ImageCanvasBasicGenerationComposerView.tsx index 24a996b18..9a83ecc9d 100644 --- a/src/components/image-editor/ImageCanvasBasicGenerationComposerView.tsx +++ b/src/components/image-editor/ImageCanvasBasicGenerationComposerView.tsx @@ -1,4 +1,4 @@ -import { ImageIcon, X } from 'lucide-react'; +import { ImageIcon } from 'lucide-react'; import { type CSSProperties, type Dispatch, @@ -13,7 +13,6 @@ import { } from '../common/PlatformFloatingMenu'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; -import { EditorIconButton } from './ImageCanvasEditorPrimitives'; import type { CharacterReferenceImage, GenerateDialogState, @@ -42,7 +41,6 @@ type ImageCanvasBasicGenerationComposerViewProps = { onToggleReferenceMenu?: () => void; onRememberImageModel?: (model: string) => void; onSubmit: (dialog: GenerateDialogState) => void; - onClose: () => void; }; function resetFailedDialogStatus(dialog: GenerateDialogState) { return { @@ -90,7 +88,6 @@ export function ImageCanvasBasicGenerationComposerView({ onToggleReferenceMenu, onRememberImageModel = () => {}, onSubmit, - onClose, }: ImageCanvasBasicGenerationComposerViewProps) { const references = dialog.generationReferences ?? []; useImageCanvasFloatingOptionDismiss({ @@ -203,14 +200,6 @@ export function ImageCanvasBasicGenerationComposerView({ {dialog.errorMessage} ) : null} - {isGenerationReferenceMenuOpen && generationReferenceButtonRef ? renderEditorPortal( diff --git a/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx b/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx index b9e4520e7..69adbf95e 100644 --- a/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx +++ b/src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx @@ -608,13 +608,75 @@ describe('ImageCanvasEditorView generation integration', () => { expect(screen.getByLabelText('图像生成占位图')).toBeTruthy(); }); - it('closes the generation composer without removing the placeholder frame', () => { + it('hides quick edit and redraw panels when clicking the canvas outside generation controls', () => { + render(); + const viewport = screen.getByLabelText('画布工作区'); + const sourceLayer = screen.getByAltText('画布图片:拼图素材').closest('button')!; + + fireEvent.pointerDown(sourceLayer, { + button: 0, + pointerId: 631, + clientX: 120, + clientY: 120, + }); + fireEvent.pointerUp(viewport, { + pointerId: 631, + clientX: 120, + clientY: 120, + }); + fireEvent.click(screen.getByRole('button', { name: '快速编辑' })); + expect(screen.getByRole('dialog', { name: '快速编辑图片' })).toBeTruthy(); + expect( + screen.queryByRole('button', { name: '关闭快速编辑图片' }), + ).toBeNull(); + + fireEvent.pointerDown(viewport, { + button: 0, + pointerId: 632, + clientX: 260, + clientY: 180, + }); + + expect( + screen.queryByRole('dialog', { name: '快速编辑图片' }), + ).toBeNull(); + + fireEvent.pointerDown(sourceLayer, { + button: 0, + pointerId: 633, + clientX: 120, + clientY: 120, + }); + fireEvent.pointerUp(viewport, { + pointerId: 633, + clientX: 120, + clientY: 120, + }); + fireEvent.click(screen.getByRole('button', { name: '改造' })); + expect(screen.getByRole('dialog', { name: '重绘图片' })).toBeTruthy(); + expect( + screen.queryByRole('button', { name: '关闭重绘图片' }), + ).toBeNull(); + + fireEvent.pointerDown(viewport, { + button: 0, + pointerId: 634, + clientX: 280, + clientY: 190, + }); + + expect(screen.queryByRole('dialog', { name: '重绘图片' })).toBeNull(); + }); + + it('does not render a standalone close button on the generation composer', () => { render(); fireEvent.click(screen.getByRole('button', { name: '生成图片' })); - fireEvent.click(screen.getByRole('button', { name: '关闭生成图片' })); - expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull(); + expect( + screen.queryByRole('button', { name: '关闭生成图片' }), + ).toBeNull(); + expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy(); expect(screen.getByLabelText('图像生成占位图')).toBeTruthy(); }); diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index 77b6803d5..a6130e7e8 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -246,6 +246,7 @@ export function ImageCanvasEditorView() { updateCanvasGenerationDialogById, removeCanvasGenerationDialogById, hasCanvasGenerationDialogById, + archiveActiveCanvasGenerationDialog, activateCanvasGenerationDialog, restoreCanvasGenerationDialogs, removeCanvasGenerationDialogsByLayerId, @@ -530,6 +531,7 @@ export function ImageCanvasEditorView() { openCanvasGenerationDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, + archiveActiveCanvasGenerationDialog, removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, @@ -546,6 +548,8 @@ export function ImageCanvasEditorView() { }); const { setQuickEditPanel, + setCropExpandPanel, + setCharacterAnimationPanel, setIsSpecMenuOpen, setIsGenerationReferenceMenuOpen, setIsCharacterSpecMenuOpen, @@ -586,6 +590,11 @@ export function ImageCanvasEditorView() { switchGenerationTool, } = generationSurface; setQuickEditPanelRef.current = setQuickEditPanel; + const clearGenerationPanelsAfterBlur = useCallback(() => { + setQuickEditPanel(null); + setCropExpandPanel(null); + setCharacterAnimationPanel(null); + }, [setCharacterAnimationPanel, setCropExpandPanel, setQuickEditPanel]); const { selectedLayer, selectedToolbarStyle, @@ -607,6 +616,7 @@ export function ImageCanvasEditorView() { canvasSize, selectSingleLayer, hideGeneratedLayerPanelAfterBlur, + clearGenerationPanelsAfterBlur, getCanvasPointFromClient, }); const { diff --git a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx index 27d0b4901..99aabbec2 100644 --- a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx +++ b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx @@ -97,7 +97,6 @@ function createComposerProps( onSubmitQuickEdit: vi.fn(), onSubmitCropExpand: vi.fn(), onSubmitCharacterAnimation: vi.fn(), - onCloseGenerateComposer: vi.fn(), onUpdateSpecFormValue: vi.fn(), onUpdateIconDescriptionText: vi.fn(), onUpdateCharacterAnimationDuration: vi.fn(), diff --git a/src/components/image-editor/ImageCanvasGenerationComposerView.tsx b/src/components/image-editor/ImageCanvasGenerationComposerView.tsx index 5822ad835..3d406c815 100644 --- a/src/components/image-editor/ImageCanvasGenerationComposerView.tsx +++ b/src/components/image-editor/ImageCanvasGenerationComposerView.tsx @@ -118,7 +118,6 @@ type ImageCanvasGenerationComposerViewProps = { onSubmitQuickEdit: () => void; onSubmitCropExpand: () => void; onSubmitCharacterAnimation: () => void; - onCloseGenerateComposer: () => void; onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void; onUpdateIconDescriptionText: (value: string) => void; onUpdateCharacterAnimationDuration: (frameCountValue: string) => void; @@ -883,7 +882,6 @@ export function ImageCanvasGenerationComposerView({ onSubmitQuickEdit, onSubmitCropExpand, onSubmitCharacterAnimation, - onCloseGenerateComposer, onUpdateSpecFormValue, onUpdateIconDescriptionText, onUpdateCharacterAnimationDuration, @@ -939,7 +937,6 @@ export function ImageCanvasGenerationComposerView({ } onRememberImageModel={onRememberImageModel} onSubmit={onSubmitImageGeneration} - onClose={onCloseGenerateComposer} /> ) : null} diff --git a/src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx b/src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx index 401ab04d9..a82c48f5a 100644 --- a/src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx +++ b/src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx @@ -114,7 +114,7 @@ describe('ImageCanvasQuickEditPanelView', () => { ).toBe(true); }); - it('submits and closes through its interface', () => { + it('submits without rendering a standalone close button', () => { const submitQuickEdit = vi.fn(); render( { ); fireEvent.click(screen.getByRole('button', { name: '生成' })); - fireEvent.click(screen.getByRole('button', { name: '关闭快速编辑图片' })); expect(submitQuickEdit).toHaveBeenCalledTimes(1); - expect(screen.getByLabelText('面板状态').textContent).toBe('closed'); + expect( + screen.queryByRole('button', { name: '关闭快速编辑图片' }), + ).toBeNull(); }); it('shows configured mud point price inside redraw submit button', () => { diff --git a/src/components/image-editor/ImageCanvasQuickEditPanelView.tsx b/src/components/image-editor/ImageCanvasQuickEditPanelView.tsx index 062339c1e..ab7fde03e 100644 --- a/src/components/image-editor/ImageCanvasQuickEditPanelView.tsx +++ b/src/components/image-editor/ImageCanvasQuickEditPanelView.tsx @@ -1,4 +1,4 @@ -import { Cpu, ImageIcon, ImagePlus, X } from 'lucide-react'; +import { Cpu, ImageIcon, ImagePlus } from 'lucide-react'; import { type CSSProperties, type Dispatch, @@ -15,7 +15,6 @@ import { import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; import { PlatformTextField } from '../common/PlatformTextField'; -import { EditorIconButton } from './ImageCanvasEditorPrimitives'; import type { CanvasLayer, CharacterReferenceImage, @@ -235,15 +234,6 @@ export function ImageCanvasQuickEditPanelView({ )} - setQuickEditPanel(null)} - /> {!isRedraw && isReferenceMenuOpen && referenceButtonRef ? renderEditorPortal( diff --git a/src/components/image-editor/ImageCanvasWorldView.test.tsx b/src/components/image-editor/ImageCanvasWorldView.test.tsx index 9ba108644..e9729a727 100644 --- a/src/components/image-editor/ImageCanvasWorldView.test.tsx +++ b/src/components/image-editor/ImageCanvasWorldView.test.tsx @@ -69,6 +69,7 @@ function renderWorldView( onLayerMouseEnter: vi.fn(), onLayerMouseLeave: vi.fn(), onOpenLayerMetadata: vi.fn(), + onUpdateLayerAssetKind: vi.fn(), onGenerationFramePointerDown: vi.fn(), onActivateGenerationDialog: vi.fn(), onCropExpandHandlePointerDown: vi.fn(), @@ -145,7 +146,7 @@ describe('ImageCanvasWorldView', () => { expect(screen.getByText('角色')).toBeTruthy(); }); - it('shows kind tag for tagged layers and omits the tag for untagged layers', () => { + it('shows editable kind tags for tagged and untagged layers', () => { const layer = createLayer({ assetKind: null }); renderWorldView({ layers: [ @@ -164,7 +165,7 @@ describe('ImageCanvasWorldView', () => { name: '选择角色标签图层', }); - expect(within(layerButton).queryByText('未知')).toBeNull(); + expect(within(layerButton).getByText('未知')).toBeTruthy(); expect(within(characterButton).getByText('角色')).toBeTruthy(); }); diff --git a/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx b/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx index 260838c9c..e1b70d30e 100644 --- a/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx @@ -95,6 +95,8 @@ function GenerationSurfaceHarness() { openCanvasGenerationDialog: dialogs.openCanvasGenerationDialog, updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById, hasCanvasGenerationDialogById: dialogs.hasCanvasGenerationDialogById, + archiveActiveCanvasGenerationDialog: + dialogs.archiveActiveCanvasGenerationDialog, removeCanvasGenerationDialogsByLayerId: dialogs.removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder: dialogs.getGeneratingDialogPlaceholder, @@ -208,18 +210,17 @@ describe('useImageCanvasGenerationSurface', () => { expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy(); }); - it('derives composer position and closes the generated image composer', () => { + it('derives composer position without rendering a standalone close button', () => { render(); fireEvent.click(screen.getByRole('button', { name: '切换生成' })); expect(screen.getByTestId('composer-left').textContent).toBe('450'); expect(screen.getByTestId('composer-top').textContent).toBe('1354'); - - fireEvent.click(screen.getByRole('button', { name: '关闭生成图片' })); - expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull(); - expect(screen.getByTestId('tool').textContent).toBe('select'); + expect( + screen.queryByRole('button', { name: '关闭生成图片' }), + ).toBeNull(); expect(screen.getByTestId('dialog').textContent).toBe( - 'generate:closed:placeholder', + 'generate:open:placeholder', ); }); diff --git a/src/components/image-editor/useImageCanvasGenerationSurface.tsx b/src/components/image-editor/useImageCanvasGenerationSurface.tsx index ac1d6f75c..d7ad480a1 100644 --- a/src/components/image-editor/useImageCanvasGenerationSurface.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSurface.tsx @@ -71,6 +71,7 @@ type ImageCanvasGenerationSurfaceOptions = { updater: CanvasGenerationDialogUpdater, ) => void; hasCanvasGenerationDialogById: (dialogId: string) => boolean; + archiveActiveCanvasGenerationDialog: () => void; removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void; getGeneratingDialogPlaceholder: ( dialog: GenerateDialogState, @@ -145,6 +146,7 @@ export function useImageCanvasGenerationSurface({ openCanvasGenerationDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, + archiveActiveCanvasGenerationDialog, removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, @@ -175,6 +177,7 @@ export function useImageCanvasGenerationSurface({ openCanvasGenerationDialog, updateCanvasGenerationDialogById, hasCanvasGenerationDialogById, + archiveActiveCanvasGenerationDialog, removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder, appendCanvasLayersWithResources, @@ -426,7 +429,6 @@ export function useImageCanvasGenerationSurface({ onSubmitCharacterAnimation={() => void generationWorkflow.submitCharacterAnimation() } - onCloseGenerateComposer={generationWorkflow.closeGenerateComposer} onUpdateSpecFormValue={generationWorkflow.updateSpecFormValue} onUpdateIconDescriptionText={ generationWorkflow.updateIconDescriptionsText diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx index 7406583cc..af5c6b59d 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx @@ -128,6 +128,8 @@ function GenerationWorkflowHarness({ removeCanvasGenerationDialogsByLayerId: dialogs.removeCanvasGenerationDialogsByLayerId, getGeneratingDialogPlaceholder: dialogs.getGeneratingDialogPlaceholder, + archiveActiveCanvasGenerationDialog: + dialogs.archiveActiveCanvasGenerationDialog, appendCanvasLayersWithResources: (nextLayers) => setLayers((currentLayers) => [...currentLayers, ...nextLayers]), selectSingleLayer: setSelectedLayerId, @@ -171,6 +173,14 @@ function GenerationWorkflowHarness({ ? `${activeDialog.mode}:${activeDialog.status}:${activeDialog.composerOpen !== false ? 'open' : 'closed'}:${activeDialog.generatedLayerId ?? '-'}:${activeDialog.placeholder ? 'placeholder' : '-'}` : '-'} + + {dialogs.canvasGenerationDialogs + .map( + (dialog) => + `${dialog.id}:${dialog.mode}:${dialog.composerOpen !== false ? 'open' : 'closed'}`, + ) + .join('|') || '-'} + {activeDialog?.generationReferences ?.map((reference) => reference.label) @@ -254,6 +264,9 @@ function GenerationWorkflowHarness({ > 打开快速编辑 +