diff --git a/src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx b/src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx index a67baa288..691b009b5 100644 --- a/src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx +++ b/src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx @@ -31,11 +31,17 @@ describe('ImageCanvasBottomToolbarView', () => { ).toBe('false'); fireEvent.click(within(toolbar).getByRole('button', { name: '抓手工具' })); + fireEvent.click(within(toolbar).getByRole('button', { name: '生成视频' })); fireEvent.click(within(toolbar).getByRole('button', { name: '生成规范' })); + fireEvent.click( + within(toolbar).getByRole('button', { name: '生成UI设计图' }), + ); fireEvent.click(within(toolbar).getByRole('button', { name: '文字工具' })); expect(switchTool).toHaveBeenNthCalledWith(1, 'hand'); - expect(switchTool).toHaveBeenNthCalledWith(2, 'spec'); - expect(switchTool).toHaveBeenNthCalledWith(3, 'text'); + expect(switchTool).toHaveBeenNthCalledWith(2, 'video'); + expect(switchTool).toHaveBeenNthCalledWith(3, 'spec'); + expect(switchTool).toHaveBeenNthCalledWith(4, 'ui-design'); + expect(switchTool).toHaveBeenNthCalledWith(5, 'text'); }); }); diff --git a/src/components/image-editor/ImageCanvasBottomToolbarView.tsx b/src/components/image-editor/ImageCanvasBottomToolbarView.tsx index dd36b4cce..8075cf122 100644 --- a/src/components/image-editor/ImageCanvasBottomToolbarView.tsx +++ b/src/components/image-editor/ImageCanvasBottomToolbarView.tsx @@ -1,9 +1,11 @@ import { ClipboardList, + Clapperboard, Download, Hand, ImageIcon, ImagePlus, + LayoutTemplate, MousePointer2, Shapes, Sparkles, @@ -30,9 +32,11 @@ const canvasTools: Array<{ { id: 'hand', label: '抓手工具', icon: Hand }, { id: 'upload', label: '上传工具', icon: ImagePlus }, { id: 'generate', label: '生成工具', icon: WandSparkles }, + { id: 'video', label: '生成视频', icon: Clapperboard }, { id: 'spec', label: '生成规范', icon: ClipboardList }, { id: 'character', label: '生成角色形象', icon: Sparkles }, { id: 'icon', label: '生成图标素材', icon: ImageIcon }, + { id: 'ui-design', label: '生成UI设计图', icon: LayoutTemplate }, { id: 'text', label: '文字工具', icon: Type }, { id: 'shape', label: '形状标注工具', icon: Shapes }, { id: 'export', label: '导出工具', icon: Download }, diff --git a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx index 23089a2f8..8e2eab8ae 100644 --- a/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx +++ b/src/components/image-editor/ImageCanvasGenerationComposerView.test.tsx @@ -94,6 +94,7 @@ function renderComposer( describe('ImageCanvasGenerationComposerView', () => { it('让生成UI设计图面板复用普通图片生成面板的纵向结构', () => { + const setGenerateDialog = vi.fn(); renderComposer({ mode: 'ui-design', prompt: '', @@ -103,6 +104,12 @@ describe('ImageCanvasGenerationComposerView', () => { imageModel: 'gpt-image-2', aspectRatio: '16:9', imageSize: '1K', + }, { + isUiDesignSpecMenuOpen: true, + setGenerateDialog: + setGenerateDialog as unknown as Dispatch< + SetStateAction + >, }); const panel = screen.getByRole('dialog', { name: '生成UI设计图' }); @@ -122,6 +129,14 @@ describe('ImageCanvasGenerationComposerView', () => { expect( panel.querySelector('.image-canvas-editor__generation-composer-footer'), ).toBeTruthy(); + fireEvent.change(within(panel).getByRole('textbox', { name: 'UI设计要求' }), { + target: { value: '主界面和结算弹窗' }, + }); + expect(setGenerateDialog).toHaveBeenCalled(); + const menu = screen.getByRole('menu', { name: '参考图来源' }); + expect( + within(menu).getByRole('menuitem', { name: '新建图标素材规范' }), + ).toBeTruthy(); }); it('让生成规范图片面板复用生成类面板 shell 和底部按钮结构', () => { @@ -256,6 +271,50 @@ describe('ImageCanvasGenerationComposerView', () => { within(menu).getByRole('menuitem', { name: '上传图片' }), ).toBeTruthy(); }); + + it('生成视频面板可切换时长、清晰度和模型并更新泥点', () => { + const setGenerateDialog = vi.fn(); + + renderComposer( + { + mode: 'video', + prompt: '', + status: 'idle', + composerOpen: true, + generationReferences: [], + videoModel: 'seedance2.0', + videoAspectRatio: '16:9', + videoDurationSeconds: 4, + videoResolution: '480p', + videoMode: 'std', + videoSound: 'off', + }, + { + setGenerateDialog: + setGenerateDialog as unknown as Dispatch< + SetStateAction + >, + }, + ); + + const panel = screen.getByRole('dialog', { name: '生成视频' }); + expect(within(panel).getByRole('button', { name: '视频参数 16:9 · 4秒 · 480p' })) + .toBeTruthy(); + expect(within(panel).getByRole('button', { name: '模型 Seedance 2.0' })) + .toBeTruthy(); + expect(within(panel).getByRole('button', { name: '清晰度 480p' })) + .toBeTruthy(); + expect(within(panel).getByRole('button', { name: '生成视频' }).textContent) + .toBe('40'); + + fireEvent.click( + within(panel).getByRole('button', { name: '视频参数 16:9 · 4秒 · 480p' }), + ); + fireEvent.click(within(panel).getByRole('button', { name: '模型 Seedance 2.0' })); + fireEvent.click(within(panel).getByRole('button', { name: '清晰度 480p' })); + + expect(setGenerateDialog).toHaveBeenCalledTimes(3); + }); it('生成规范参考图点击先弹来源菜单,不直接打开上传', () => { const onRequestUpload = vi.fn(); const setIsGenerationReferenceMenuOpen = vi.fn(); @@ -293,4 +352,3 @@ describe('ImageCanvasGenerationComposerView', () => { expect(within(menu).getByRole('menuitem', { name: '上传图片' })).toBeTruthy(); }); }); - diff --git a/src/components/image-editor/ImageCanvasGenerationComposerView.tsx b/src/components/image-editor/ImageCanvasGenerationComposerView.tsx index 668ba6dcf..5cb012d95 100644 --- a/src/components/image-editor/ImageCanvasGenerationComposerView.tsx +++ b/src/components/image-editor/ImageCanvasGenerationComposerView.tsx @@ -1,4 +1,4 @@ -import { ImageIcon } from 'lucide-react'; +import { ChevronDown, ImageIcon } from 'lucide-react'; import { type CSSProperties, type Dispatch, @@ -14,6 +14,7 @@ import { } from '../common/PlatformFloatingMenu'; import { PlatformActionButton } from '../common/PlatformActionButton'; import { PlatformIconButton } from '../common/PlatformIconButton'; +import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; import { PlatformTextField } from '../common/PlatformTextField'; import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView'; import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAnimationPanelView'; @@ -23,6 +24,7 @@ import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSprites import { ImageCanvasQuickEditPanelView } from './ImageCanvasQuickEditPanelView'; import { ImageCanvasSpecGenerationPanelView } from './ImageCanvasSpecGenerationPanelView'; import { + EDITOR_VIDEO_MODEL_OPTIONS, SPEC_TYPE_LABEL, calculateEditorVideoPrice, } from './ImageCanvasGenerationModel'; @@ -155,6 +157,9 @@ function ImageCanvasVideoGenerationComposerView({ }) { const resolution = dialog.videoResolution ?? '480p'; const durationSeconds = dialog.videoDurationSeconds ?? 4; + const currentModel = + EDITOR_VIDEO_MODEL_OPTIONS.find((item) => item.value === dialog.videoModel) + ?? EDITOR_VIDEO_MODEL_OPTIONS[0]; const price = calculateEditorVideoPrice(resolution, durationSeconds); return ( <> @@ -211,9 +216,97 @@ function ImageCanvasVideoGenerationComposerView({ } />
- + } + onClick={() => { + setGenerateDialog((currentDialog) => + currentDialog?.mode === 'video' + ? { + ...currentDialog, + videoDurationSeconds: + currentDialog.videoDurationSeconds === 5 ? 4 : 5, + status: + currentDialog.status === 'failed' + ? 'idle' + : currentDialog.status, + errorMessage: + currentDialog.status === 'failed' + ? undefined + : currentDialog.errorMessage, + } + : currentDialog, + ); + }} + > 16:9 · {resolution} · {durationSeconds}秒 - + + } + onClick={() => { + setGenerateDialog((currentDialog) => { + if (currentDialog?.mode !== 'video') { + return currentDialog; + } + const currentIndex = EDITOR_VIDEO_MODEL_OPTIONS.findIndex( + (item) => item.value === currentDialog.videoModel, + ); + const nextModel = + EDITOR_VIDEO_MODEL_OPTIONS[ + (Math.max(currentIndex, 0) + 1) % + EDITOR_VIDEO_MODEL_OPTIONS.length + ]; + return { + ...currentDialog, + videoModel: nextModel.value, + status: + currentDialog.status === 'failed' + ? 'idle' + : currentDialog.status, + errorMessage: + currentDialog.status === 'failed' + ? undefined + : currentDialog.errorMessage, + }; + }); + }} + > + {currentModel.label} + + } + onClick={() => { + setGenerateDialog((currentDialog) => + currentDialog?.mode === 'video' + ? { + ...currentDialog, + videoResolution: + currentDialog.videoResolution === '720p' + ? '480p' + : '720p', + status: + currentDialog.status === 'failed' + ? 'idle' + : currentDialog.status, + errorMessage: + currentDialog.status === 'failed' + ? undefined + : currentDialog.errorMessage, + } + : currentDialog, + ); + }} + > + {resolution} + { }); }); + it('creates video and UI design drafts restored from the original generation entry work', () => { + const canvasSize = { width: 960, height: 720 }; + const viewport = { x: 0, y: 0, scale: 1 }; + + expect(createVideoGenerationDialogDraft({ canvasSize, viewport })) + .toMatchObject({ + mode: 'video', + status: 'idle', + composerOpen: true, + generationReferences: [], + videoModel: 'seedance2.0', + videoAspectRatio: '16:9', + videoResolution: '480p', + videoDurationSeconds: 4, + videoMode: 'std', + videoSound: 'off', + placeholder: { + x: 200, + y: 202.5, + width: 560, + height: 315, + originalWidth: 1280, + originalHeight: 720, + }, + }); + expect( + createUiDesignGenerationDialogDraft({ + canvasSize, + viewport, + imageModel: 'gpt-image-2', + }), + ).toMatchObject({ + mode: 'ui-design', + imageModel: 'gpt-image-2', + aspectRatio: '16:9', + imageSize: '1K', + uiDesignSpecReference: null, + placeholder: { + x: 200, + y: 202.5, + width: 560, + height: 315, + }, + }); + }); + it('creates edit, quick-edit, and character animation panel drafts', () => { const sourceLayer = createLayer({ prompt: '原图提示', @@ -214,6 +264,56 @@ describe('ImageCanvasGenerationDialogModel', () => { expect(assignIconSpecReference(iconDialog, sourceLayer)).toBe(iconDialog); }); + it('routes picked canvas references to spec, image, video, and UI design fields', () => { + const sourceLayer = createLayer({ title: '参考图' }); + expect( + appendGenerationReference( + { + mode: 'spec', + prompt: '', + status: 'failed', + errorMessage: '失败', + }, + sourceLayer, + ), + ).toMatchObject({ + status: 'idle', + errorMessage: undefined, + specReference: { label: '参考图' }, + }); + expect( + appendGenerationReference( + { + mode: 'video', + prompt: '', + status: 'idle', + generationReferences: [], + }, + sourceLayer, + ), + ).toMatchObject({ + generationReferences: [{ label: '参考图' }], + }); + + const uiDialog: GenerateDialogState = { + mode: 'ui-design', + prompt: '', + status: 'failed', + errorMessage: '失败', + }; + expect(assignUiDesignSpecReference(uiDialog, sourceLayer)).toBe(uiDialog); + expect( + assignUiDesignSpecReference( + uiDialog, + createLayer({ title: '图标规范', assetKind: 'icon-spec' }), + ), + ).toMatchObject({ + status: 'idle', + errorMessage: undefined, + uiDesignSpecReference: { label: '图标规范' }, + }); + }); + it('updates failed spec and icon dialog fields back to idle state', () => { const specDialog: GenerateDialogState = { mode: 'spec', diff --git a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts index cb0e63a10..dcc038748 100644 --- a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts @@ -18,6 +18,7 @@ import { DEFAULT_IMAGE_MODEL, DEFAULT_SPEC_FORM_VALUES, EDITOR_IMAGE_DIMENSION_OPTIONS, + EDITOR_VIDEO_MODEL_OPTIONS, ICON_DESCRIPTION_LIMIT, ICON_FRAME_DISPLAY_SIZE, ICON_FRAME_ORIGINAL_SIZE, @@ -25,6 +26,8 @@ import { SPEC_FRAME_ORIGINAL_SIZE, UI_DESIGN_FRAME_DISPLAY_SIZE, UI_DESIGN_FRAME_ORIGINAL_SIZE, + VIDEO_FRAME_DISPLAY_SIZE, + VIDEO_FRAME_ORIGINAL_SIZE, } from './ImageCanvasGenerationModel'; type CanvasSize = { width: number; height: number }; @@ -182,6 +185,37 @@ export function createIconGenerationDialogDraft({ }; } +export function createVideoGenerationDialogDraft({ + canvasSize, + viewport, +}: { + canvasSize: CanvasSize; + viewport: CanvasViewport; +}): Omit { + const worldCenter = getViewportWorldCenter({ canvasSize, viewport }); + return { + mode: 'video', + prompt: '', + status: 'idle', + composerOpen: true, + generationReferences: [], + videoModel: EDITOR_VIDEO_MODEL_OPTIONS[0].value, + videoAspectRatio: '16:9', + videoResolution: '480p', + videoDurationSeconds: 4, + videoMode: 'std', + videoSound: 'off', + placeholder: { + x: worldCenter.x - VIDEO_FRAME_DISPLAY_SIZE.width / 2, + y: worldCenter.y - VIDEO_FRAME_DISPLAY_SIZE.height / 2, + width: VIDEO_FRAME_DISPLAY_SIZE.width, + height: VIDEO_FRAME_DISPLAY_SIZE.height, + originalWidth: VIDEO_FRAME_ORIGINAL_SIZE.width, + originalHeight: VIDEO_FRAME_ORIGINAL_SIZE.height, + }, + }; +} + export function createUiDesignGenerationDialogDraft({ canvasSize, viewport, @@ -200,7 +234,7 @@ export function createUiDesignGenerationDialogDraft({ composerOpen: true, uiDesignSpecReference: null, imageModel, - aspectRatio: dimensionDefaults.aspectRatio, + aspectRatio: '16:9', imageSize: dimensionDefaults.imageSize, placeholder: { x: worldCenter.x - UI_DESIGN_FRAME_DISPLAY_SIZE.width / 2, @@ -292,9 +326,14 @@ export function appendGenerationReference( dialog: GenerateDialogState | null, layer: CanvasLayer, ): GenerateDialogState | null { - return dialog?.mode === 'generate' || - dialog?.mode === 'video' || - dialog?.mode === 'spec' + if (dialog?.mode === 'spec') { + return { + ...resetFailedGenerationDialog(dialog), + specReference: createCanvasLayerReference(layer), + composerOpen: true, + }; + } + return dialog?.mode === 'generate' || dialog?.mode === 'video' ? { ...resetFailedGenerationDialog(dialog), generationReferences: [ @@ -326,6 +365,9 @@ export function assignUiDesignSpecReference( dialog: GenerateDialogState | null, layer: CanvasLayer, ): GenerateDialogState | null { + if (layer.assetKind !== 'icon-spec') { + return dialog; + } return dialog?.mode === 'ui-design' ? { ...resetFailedGenerationDialog(dialog), diff --git a/src/components/image-editor/ImageCanvasOverlayModel.ts b/src/components/image-editor/ImageCanvasOverlayModel.ts index 59492c8b5..5ef8e07ac 100644 --- a/src/components/image-editor/ImageCanvasOverlayModel.ts +++ b/src/components/image-editor/ImageCanvasOverlayModel.ts @@ -183,6 +183,8 @@ export function isCanvasGenerationComposerVisible( dialog?.mode === 'generate' || dialog?.mode === 'spec' || dialog?.mode === 'character' || - dialog?.mode === 'icon' + dialog?.mode === 'icon' || + dialog?.mode === 'ui-design' || + dialog?.mode === 'video' ); } diff --git a/src/components/image-editor/ImageCanvasSpecGenerationPanelView.tsx b/src/components/image-editor/ImageCanvasSpecGenerationPanelView.tsx index 2fef0779c..4bf5fee8c 100644 --- a/src/components/image-editor/ImageCanvasSpecGenerationPanelView.tsx +++ b/src/components/image-editor/ImageCanvasSpecGenerationPanelView.tsx @@ -25,6 +25,7 @@ import { import type { GenerateDialogState, SpecFormValues, + SpecGenerationType, UploadTarget, } from './ImageCanvasEditorTypes'; @@ -35,11 +36,13 @@ type ImageCanvasSpecGenerationPanelViewProps = { generationReferenceButtonRef?: RefObject; setIsGenerationReferenceMenuOpen?: Dispatch>; setIsPickingGenerationReferenceFromCanvas?: Dispatch>; + setGenerateDialog?: Dispatch>; renderEditorPortal?: (node: ReactNode) => ReactNode; buildPortalMenuStyle?: ( anchor: HTMLElement | null, placement: 'above' | 'below', ) => CSSProperties; + onOpenSpecDialog?: (specType: SpecGenerationType) => void; onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void; onRequestUpload: (target: UploadTarget) => void; onSubmit: (dialog: GenerateDialogState) => void; @@ -52,8 +55,10 @@ export function ImageCanvasSpecGenerationPanelView({ generationReferenceButtonRef, setIsGenerationReferenceMenuOpen, setIsPickingGenerationReferenceFromCanvas, + setGenerateDialog, renderEditorPortal = (node) => node, buildPortalMenuStyle = () => ({}), + onOpenSpecDialog, onUpdateSpecFormValue, onRequestUpload, onSubmit, @@ -107,9 +112,29 @@ export function ImageCanvasSpecGenerationPanelView({ size="sm" density="compact" className="image-canvas-editor__generation-prompt" - onChange={(event) => - onUpdateSpecFormValue('customPrompt', event.target.value) - } + onChange={(event) => { + const nextPrompt = event.target.value; + if (setGenerateDialog) { + setGenerateDialog((currentDialog) => + currentDialog?.mode === 'ui-design' + ? { + ...currentDialog, + prompt: nextPrompt, + status: + currentDialog.status === 'failed' + ? 'idle' + : currentDialog.status, + errorMessage: + currentDialog.status === 'failed' + ? undefined + : currentDialog.errorMessage, + } + : currentDialog, + ); + return; + } + onUpdateSpecFormValue('customPrompt', nextPrompt); + }} /> ) : dialog.specType === 'custom' ? ( @@ -304,6 +329,16 @@ export function ImageCanvasSpecGenerationPanelView({ > 从画布中选择 + {isUiDesignDialog ? ( + { + setIsGenerationReferenceMenuOpen?.(false); + onOpenSpecDialog?.('icon'); + }} + > + 新建图标素材规范 + + ) : null} { setIsGenerationReferenceMenuOpen?.(false); diff --git a/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx b/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx index da0919ecc..23c8bab94 100644 --- a/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSurface.test.tsx @@ -125,6 +125,9 @@ function GenerationSurfaceHarness() { > 切换生成 + @@ -137,6 +140,12 @@ function GenerationSurfaceHarness() { + @@ -183,4 +192,22 @@ describe('useImageCanvasGenerationSurface', () => { const menu = screen.getByRole('menu', { name: '生成规范类型' }); expect(within(menu).getByRole('menuitem', { name: '角色形象规范' })).toBeTruthy(); }); + + it('opens video and UI design generation dialogs through the generation surface', () => { + render(); + + fireEvent.click(screen.getByRole('button', { name: '切换视频' })); + expect(screen.getByTestId('tool').textContent).toBe('video'); + expect(screen.getByTestId('dialog').textContent).toBe( + 'video:open:placeholder', + ); + expect(screen.getByRole('dialog', { name: '生成视频' })).toBeTruthy(); + + fireEvent.click(screen.getByRole('button', { name: '切换UI设计' })); + expect(screen.getByTestId('tool').textContent).toBe('ui-design'); + expect(screen.getByTestId('dialog').textContent).toBe( + 'ui-design:open:placeholder', + ); + expect(screen.getByRole('dialog', { name: '生成UI设计图' })).toBeTruthy(); + }); }); diff --git a/src/components/image-editor/useImageCanvasGenerationSurface.tsx b/src/components/image-editor/useImageCanvasGenerationSurface.tsx index 05da70962..a0b822a17 100644 --- a/src/components/image-editor/useImageCanvasGenerationSurface.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSurface.tsx @@ -158,6 +158,10 @@ export function useImageCanvasGenerationSurface({ generationWorkflow.openGenerateDialog(); return true; } + if (tool === 'video') { + generationWorkflow.openVideoGenerationDialog(); + return true; + } if (tool === 'spec') { generationWorkflow.setIsSpecMenuOpen((open) => !open); setActiveTool('spec'); @@ -171,6 +175,10 @@ export function useImageCanvasGenerationSurface({ generationWorkflow.openIconGenerationDialog(); return true; } + if (tool === 'ui-design') { + generationWorkflow.openUiDesignGenerationDialog(); + return true; + } return false; }, [generationWorkflow, setActiveTool], diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts index cdbb65429..2308a2c59 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts @@ -35,6 +35,8 @@ import { createIconGenerationDialogDraft, createQuickEditPanelDraft, createSpecDialogDraft, + createUiDesignGenerationDialogDraft, + createVideoGenerationDialogDraft, hideGeneratedLayerComposerAfterBlur, updateCharacterAnimationDurationPanel, updateIconDescriptionInDialog, @@ -300,6 +302,62 @@ export function useImageCanvasGenerationWorkflow({ viewport, ]); + const openVideoGenerationDialog = useCallback(() => { + setIsSpecMenuOpen(false); + setIsGenerationReferenceMenuOpen(false); + setIsCharacterReferenceMenuOpen(false); + setIsPickingGenerationReferenceFromCanvas(false); + setIsPickingCharacterSpecFromCanvas(false); + setIsPickingCharacterReferenceFromCanvas(false); + setIsIconSpecMenuOpen(false); + setIsPickingIconSpecFromCanvas(false); + setIsUiDesignSpecMenuOpen(false); + setIsPickingUiDesignSpecFromCanvas(false); + openCanvasGenerationDialog( + createVideoGenerationDialogDraft({ canvasSize, viewport }), + ); + setActiveTool('video'); + selectSingleLayer(null); + setQuickEditPanel(null); + setCharacterAnimationPanel(null); + }, [ + canvasSize, + openCanvasGenerationDialog, + selectSingleLayer, + setActiveTool, + viewport, + ]); + + const openUiDesignGenerationDialog = useCallback(() => { + setIsSpecMenuOpen(false); + setIsGenerationReferenceMenuOpen(false); + setIsCharacterReferenceMenuOpen(false); + setIsPickingGenerationReferenceFromCanvas(false); + setIsPickingCharacterSpecFromCanvas(false); + setIsPickingCharacterReferenceFromCanvas(false); + setIsIconSpecMenuOpen(false); + setIsPickingIconSpecFromCanvas(false); + setIsUiDesignSpecMenuOpen(false); + setIsPickingUiDesignSpecFromCanvas(false); + openCanvasGenerationDialog( + createUiDesignGenerationDialogDraft({ + canvasSize, + viewport, + imageModel: 'gpt-image-2', + }), + ); + setActiveTool('ui-design'); + selectSingleLayer(null); + setQuickEditPanel(null); + setCharacterAnimationPanel(null); + }, [ + canvasSize, + openCanvasGenerationDialog, + selectSingleLayer, + setActiveTool, + viewport, + ]); + const openEditDialog = useCallback( (sourceLayer: CanvasLayer) => { setMetadataLayer(null); @@ -521,6 +579,8 @@ export function useImageCanvasGenerationWorkflow({ openCharacterAnimationPanel, openCharacterGenerationDialog, openIconGenerationDialog, + openVideoGenerationDialog, + openUiDesignGenerationDialog, openEditDialog, openQuickEditPanel, pickCharacterSpecFromLayer, @@ -566,8 +626,10 @@ export function useImageCanvasGenerationWorkflow({ openEditDialog, openGenerateDialog, openIconGenerationDialog, + openUiDesignGenerationDialog, openQuickEditPanel, openSpecDialog, + openVideoGenerationDialog, pickCharacterReferenceFromLayer, pickCharacterSpecFromLayer, pickGenerationReferenceFromLayer, diff --git a/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts b/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts index 982f66369..f832d5ae0 100644 --- a/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts +++ b/src/components/image-editor/useImageCanvasKeyboardShortcuts.ts @@ -66,7 +66,8 @@ function isCanvasGenerationPlaceholderDialog( dialog?.mode === 'spec' || dialog?.mode === 'character' || dialog?.mode === 'icon' || - dialog?.mode === 'ui-design') + dialog?.mode === 'ui-design' || + dialog?.mode === 'video') ); } @@ -119,7 +120,12 @@ export function useImageCanvasKeyboardShortcuts({ if (!currentDialog || currentDialog.status === 'generating') { return currentDialog; } - if (currentDialog.mode === 'generate' || currentDialog.mode === 'spec') { + if ( + currentDialog.mode === 'generate' || + currentDialog.mode === 'spec' || + currentDialog.mode === 'ui-design' || + currentDialog.mode === 'video' + ) { return { ...currentDialog, composerOpen: false, @@ -131,9 +137,6 @@ export function useImageCanvasKeyboardShortcuts({ if (currentDialog.mode === 'icon') { return currentDialog; } - if (currentDialog.mode === 'ui-design') { - return currentDialog; - } return null; }); }; @@ -178,6 +181,8 @@ export function useImageCanvasKeyboardShortcuts({ setIsPickingCharacterReferenceFromCanvas(false); setIsIconSpecMenuOpen(false); setIsPickingIconSpecFromCanvas(false); + setIsUiDesignSpecMenuOpen(false); + setIsPickingUiDesignSpecFromCanvas(false); return; } }