恢复画布视频生成入口
恢复画布底部工具栏的视频生成和 UI 设计入口 恢复视频生成弹窗的参数、模型和提交状态处理 补齐规范参考、快捷键和生成流程相关测试
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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<GenerateDialogState | null>
|
||||
>,
|
||||
});
|
||||
|
||||
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<GenerateDialogState | null>
|
||||
>,
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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({
|
||||
}
|
||||
/>
|
||||
<div className="image-canvas-editor__generation-composer-footer">
|
||||
<span className="image-canvas-editor__generation-ratio">
|
||||
<PlatformInlineOptionButton
|
||||
className="image-canvas-editor__generation-ratio"
|
||||
aria-label={`视频参数 16:9 · ${durationSeconds}秒 · ${resolution}`}
|
||||
disabled={dialog.status === 'generating'}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
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}秒
|
||||
</span>
|
||||
</PlatformInlineOptionButton>
|
||||
<PlatformInlineOptionButton
|
||||
className="image-canvas-editor__generation-model"
|
||||
aria-label={`模型 ${currentModel.label}`}
|
||||
disabled={dialog.status === 'generating'}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
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}
|
||||
</PlatformInlineOptionButton>
|
||||
<PlatformInlineOptionButton
|
||||
className="image-canvas-editor__generation-ratio"
|
||||
aria-label={`清晰度 ${resolution}`}
|
||||
disabled={dialog.status === 'generating'}
|
||||
trailingIcon={<ChevronDown className="h-3 w-3" />}
|
||||
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}
|
||||
</PlatformInlineOptionButton>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
tone="secondary"
|
||||
@@ -378,6 +471,8 @@ export function ImageCanvasGenerationComposerView({
|
||||
}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
onOpenSpecDialog={onOpenSpecDialog}
|
||||
onUpdateSpecFormValue={onUpdateSpecFormValue}
|
||||
onRequestUpload={onRequestUpload}
|
||||
onSubmit={onSubmitImageGeneration}
|
||||
@@ -398,6 +493,8 @@ export function ImageCanvasGenerationComposerView({
|
||||
}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
onOpenSpecDialog={onOpenSpecDialog}
|
||||
onUpdateSpecFormValue={onUpdateSpecFormValue}
|
||||
onRequestUpload={onRequestUpload}
|
||||
onSubmit={onSubmitImageGeneration}
|
||||
|
||||
@@ -7,9 +7,11 @@ import type {
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
appendCharacterReference,
|
||||
appendGenerationReference,
|
||||
appendIconDescriptionToDialog,
|
||||
assignCharacterSpecReference,
|
||||
assignIconSpecReference,
|
||||
assignUiDesignSpecReference,
|
||||
closeGenerateComposerDialog,
|
||||
createCharacterAnimationPanelDraft,
|
||||
createCharacterGenerationDialogDraft,
|
||||
@@ -18,6 +20,8 @@ import {
|
||||
createIconGenerationDialogDraft,
|
||||
createQuickEditPanelDraft,
|
||||
createSpecDialogDraft,
|
||||
createUiDesignGenerationDialogDraft,
|
||||
createVideoGenerationDialogDraft,
|
||||
hideGeneratedLayerComposerAfterBlur,
|
||||
updateCharacterAnimationDurationPanel,
|
||||
updateIconDescriptionInDialog,
|
||||
@@ -132,6 +136,52 @@ describe('ImageCanvasGenerationDialogModel', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
|
||||
@@ -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<CanvasGenerationDialogState, 'id'> {
|
||||
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),
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
import type {
|
||||
GenerateDialogState,
|
||||
SpecFormValues,
|
||||
SpecGenerationType,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
|
||||
@@ -35,11 +36,13 @@ type ImageCanvasSpecGenerationPanelViewProps = {
|
||||
generationReferenceButtonRef?: RefObject<HTMLButtonElement | null>;
|
||||
setIsGenerationReferenceMenuOpen?: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingGenerationReferenceFromCanvas?: Dispatch<SetStateAction<boolean>>;
|
||||
setGenerateDialog?: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : dialog.specType === 'custom' ? (
|
||||
@@ -304,6 +329,16 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
>
|
||||
从画布中选择
|
||||
</PlatformFloatingMenuItem>
|
||||
{isUiDesignDialog ? (
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen?.(false);
|
||||
onOpenSpecDialog?.('icon');
|
||||
}}
|
||||
>
|
||||
新建图标素材规范
|
||||
</PlatformFloatingMenuItem>
|
||||
) : null}
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen?.(false);
|
||||
|
||||
@@ -125,6 +125,9 @@ function GenerationSurfaceHarness() {
|
||||
>
|
||||
切换生成
|
||||
</button>
|
||||
<button type="button" onClick={() => surface.switchGenerationTool('video')}>
|
||||
切换视频
|
||||
</button>
|
||||
<button type="button" onClick={() => surface.switchGenerationTool('spec')}>
|
||||
切换规范
|
||||
</button>
|
||||
@@ -137,6 +140,12 @@ function GenerationSurfaceHarness() {
|
||||
<button type="button" onClick={() => surface.switchGenerationTool('icon')}>
|
||||
切换图标
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => surface.switchGenerationTool('ui-design')}
|
||||
>
|
||||
切换UI设计
|
||||
</button>
|
||||
<button type="button" onClick={() => surface.switchGenerationTool('text')}>
|
||||
切换文字
|
||||
</button>
|
||||
@@ -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(<GenerationSurfaceHarness />);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user