完善画布生成面板交互
补齐普通生图参考图来源菜单和画布选择流程 接入UI设计图与视频生成面板的提交链路 让生成引用上传目标支持多种生成面板 统一图片信息弹窗断言并补充相关测试 修复图标按钮浮层锚点ref与视频生成类型契约
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { forwardRef } from 'react';
|
||||
import type {
|
||||
ButtonHTMLAttributes,
|
||||
HTMLAttributes,
|
||||
KeyboardEvent,
|
||||
LabelHTMLAttributes,
|
||||
ReactNode,
|
||||
Ref,
|
||||
} from 'react';
|
||||
|
||||
type PlatformIconButtonBaseProps = {
|
||||
@@ -52,7 +54,11 @@ type PlatformIconButtonProps =
|
||||
* 平台通用图标动作按钮。
|
||||
* 统一承接纯图标动作、图标上传 label 和带短标签的浮动图标动作。
|
||||
*/
|
||||
export function PlatformIconButton({
|
||||
export const PlatformIconButton = forwardRef<
|
||||
HTMLButtonElement | HTMLLabelElement | HTMLSpanElement,
|
||||
PlatformIconButtonProps
|
||||
>(function PlatformIconButton(
|
||||
{
|
||||
label,
|
||||
icon,
|
||||
children,
|
||||
@@ -61,7 +67,9 @@ export function PlatformIconButton({
|
||||
className,
|
||||
asChild,
|
||||
...actionProps
|
||||
}: PlatformIconButtonProps) {
|
||||
},
|
||||
ref,
|
||||
) {
|
||||
const variantClassName = {
|
||||
platformIcon: 'platform-icon-button',
|
||||
surfaceFloating:
|
||||
@@ -78,6 +86,7 @@ export function PlatformIconButton({
|
||||
return (
|
||||
<label
|
||||
{...(actionProps as LabelHTMLAttributes<HTMLLabelElement>)}
|
||||
ref={ref as Ref<HTMLLabelElement>}
|
||||
title={title}
|
||||
className={actionClassName}
|
||||
>
|
||||
@@ -112,6 +121,7 @@ export function PlatformIconButton({
|
||||
return (
|
||||
<span
|
||||
{...spanProps}
|
||||
ref={ref as Ref<HTMLSpanElement>}
|
||||
aria-disabled={disabled}
|
||||
aria-label={label}
|
||||
className={actionClassName}
|
||||
@@ -133,6 +143,7 @@ export function PlatformIconButton({
|
||||
return (
|
||||
<button
|
||||
{...buttonProps}
|
||||
ref={ref as Ref<HTMLButtonElement>}
|
||||
type={type}
|
||||
aria-label={label}
|
||||
title={title}
|
||||
@@ -142,4 +153,4 @@ export function PlatformIconButton({
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -77,7 +77,7 @@ describe('ImageCanvasBasicGenerationComposerView', () => {
|
||||
expect(screen.getByLabelText('当前提示词').textContent).toBe('新的提示');
|
||||
expect(screen.getByLabelText('当前状态').textContent).toBe('idle');
|
||||
expect(screen.getByLabelText('当前错误').textContent).toBe('-');
|
||||
expect(requestUpload).toHaveBeenCalledWith('asset');
|
||||
expect(requestUpload).not.toHaveBeenCalled();
|
||||
expect(submitGeneration).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ prompt: '新的提示', status: 'idle' }),
|
||||
);
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { ChevronDown, ImageIcon, X } from 'lucide-react';
|
||||
import { type CSSProperties, type Dispatch, type SetStateAction } from 'react';
|
||||
import {
|
||||
type CSSProperties,
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
} from 'react';
|
||||
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import {
|
||||
PlatformFloatingMenu,
|
||||
PlatformFloatingMenuItem,
|
||||
} from '../common/PlatformFloatingMenu';
|
||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
@@ -9,14 +19,23 @@ import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { EditorIconButton } from './ImageCanvasEditorPrimitives';
|
||||
import type {
|
||||
GenerateDialogState,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
|
||||
type ImageCanvasBasicGenerationComposerViewProps = {
|
||||
dialog: GenerateDialogState;
|
||||
style: CSSProperties;
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
onRequestUpload: (target: UploadTarget) => void;
|
||||
generationReferenceButtonRef?: RefObject<HTMLButtonElement | null>;
|
||||
isGenerationReferenceMenuOpen?: boolean;
|
||||
setIsGenerationReferenceMenuOpen?: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingGenerationReferenceFromCanvas?: Dispatch<SetStateAction<boolean>>;
|
||||
renderEditorPortal?: (node: ReactNode) => ReactNode;
|
||||
buildPortalMenuStyle?: (
|
||||
anchor: HTMLElement | null,
|
||||
placement: 'above' | 'below',
|
||||
) => CSSProperties;
|
||||
onRequestUpload: (target: 'generation-reference') => void;
|
||||
onToggleReferenceMenu?: () => void;
|
||||
onSubmit: (dialog: GenerateDialogState) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -37,13 +56,21 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
dialog,
|
||||
style,
|
||||
setGenerateDialog,
|
||||
generationReferenceButtonRef,
|
||||
isGenerationReferenceMenuOpen = false,
|
||||
setIsGenerationReferenceMenuOpen,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
renderEditorPortal = (node) => node,
|
||||
buildPortalMenuStyle = () => ({}),
|
||||
onRequestUpload,
|
||||
onToggleReferenceMenu,
|
||||
onSubmit,
|
||||
onClose,
|
||||
}: ImageCanvasBasicGenerationComposerViewProps) {
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="image-canvas-editor__generation-composer"
|
||||
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="生成图片"
|
||||
@@ -56,11 +83,12 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
}}
|
||||
>
|
||||
<PlatformIconButton
|
||||
ref={generationReferenceButtonRef}
|
||||
variant="surfaceFloating"
|
||||
className="image-canvas-editor__generation-ref"
|
||||
label="添加参考图"
|
||||
disabled={dialog.status === 'generating'}
|
||||
onClick={() => onRequestUpload('asset')}
|
||||
onClick={() => onToggleReferenceMenu?.()}
|
||||
icon={<ImageIcon className="h-4 w-4" />}
|
||||
>
|
||||
<span>参考图</span>
|
||||
@@ -147,5 +175,37 @@ export function ImageCanvasBasicGenerationComposerView({
|
||||
onClick={onClose}
|
||||
/>
|
||||
</form>
|
||||
{isGenerationReferenceMenuOpen && generationReferenceButtonRef
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
|
||||
label="参考图来源"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
generationReferenceButtonRef.current,
|
||||
'above',
|
||||
)}
|
||||
>
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen?.(false);
|
||||
setIsPickingGenerationReferenceFromCanvas?.(true);
|
||||
}}
|
||||
>
|
||||
从画布中选择
|
||||
</PlatformFloatingMenuItem>
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen?.(false);
|
||||
setIsPickingGenerationReferenceFromCanvas?.(false);
|
||||
onRequestUpload('generation-reference');
|
||||
}}
|
||||
>
|
||||
上传图片
|
||||
</PlatformFloatingMenuItem>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -173,9 +173,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
expect(metadataButtons[0]).toBeTruthy();
|
||||
fireEvent.click(metadataButtons[0]!);
|
||||
|
||||
const infoPanel = screen.getByRole('dialog', {
|
||||
name: /生成图片 .*图片信息/,
|
||||
});
|
||||
const infoPanel = screen.getByRole('dialog', { name: '图片信息' });
|
||||
expect(within(infoPanel).queryByText('Prompt')).toBeNull();
|
||||
expect(
|
||||
within(infoPanel).queryByRole('button', { name: '复制Prompt' }),
|
||||
@@ -1452,7 +1450,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
})[0]!,
|
||||
);
|
||||
const characterInfoPanel = screen.getByRole('dialog', {
|
||||
name: /角色形象 .*图片信息/u,
|
||||
name: '图片信息',
|
||||
});
|
||||
expect(within(characterInfoPanel).queryByText('Prompt')).toBeNull();
|
||||
expect(within(characterInfoPanel).getByText('生成输入')).toBeTruthy();
|
||||
@@ -1679,9 +1677,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
fireEvent.click(
|
||||
screen.getAllByRole('button', { name: '查看返回按钮图片信息' })[0]!,
|
||||
);
|
||||
const iconInfoPanel = screen.getByRole('dialog', {
|
||||
name: '返回按钮图片信息',
|
||||
});
|
||||
const iconInfoPanel = screen.getByRole('dialog', { name: '图片信息' });
|
||||
expect(within(iconInfoPanel).queryByText('Prompt')).toBeNull();
|
||||
expect(within(iconInfoPanel).getByText('生成输入')).toBeTruthy();
|
||||
expect(within(iconInfoPanel).getByText('素材描述 1')).toBeTruthy();
|
||||
@@ -2216,9 +2212,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
);
|
||||
fireEvent.click(metadataCornerButton);
|
||||
|
||||
const metadataDialog = screen.getByRole('dialog', {
|
||||
name: /生成图片 .*图片信息/,
|
||||
});
|
||||
const metadataDialog = screen.getByRole('dialog', { name: '图片信息' });
|
||||
expect(metadataDialog).toBeTruthy();
|
||||
expect(within(metadataDialog).getByText('图片类型')).toBeTruthy();
|
||||
expect(within(metadataDialog).getByText('生成图片')).toBeTruthy();
|
||||
@@ -2266,7 +2260,7 @@ describe('ImageCanvasEditorView generation integration', () => {
|
||||
})[0]!,
|
||||
);
|
||||
const editedMetadataDialog = screen.getByRole('dialog', {
|
||||
name: /生成图片 .* 修改结果图片信息/u,
|
||||
name: '图片信息',
|
||||
});
|
||||
expect(within(editedMetadataDialog).queryByText('Prompt')).toBeNull();
|
||||
expect(within(editedMetadataDialog).getByText('修改要求')).toBeTruthy();
|
||||
|
||||
@@ -225,7 +225,7 @@ describe('ImageCanvasEditorShellView', () => {
|
||||
expect(screen.getByRole('heading', { name: '默认项目' })).toBeTruthy();
|
||||
expect(screen.getByLabelText('画布工作区')).toBeTruthy();
|
||||
expect(screen.getByRole('toolbar', { name: 'AI画布工具栏' })).toBeTruthy();
|
||||
expect(screen.getByRole('dialog', { name: '测试图片图片信息' })).toBeTruthy();
|
||||
expect(screen.getByRole('dialog', { name: '图片信息' })).toBeTruthy();
|
||||
|
||||
const uploadInput = screen.getByLabelText('上传图片文件') as HTMLInputElement;
|
||||
fireEvent.change(uploadInput);
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
EditorCharacterAnimationGenerationResult,
|
||||
EditorCharacterAnimationRatio,
|
||||
EditorCharacterAnimationResolution,
|
||||
EditorVideoModel,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
|
||||
export type CanvasSourceType = 'uploaded' | 'generated' | 'mock_generated';
|
||||
@@ -159,9 +160,9 @@ export type GenerateDialogState = {
|
||||
iconDescriptions?: string[];
|
||||
uiDesignSpecReference?: CharacterReferenceImage | null;
|
||||
imageModel?: string;
|
||||
videoModel?: string;
|
||||
videoModel?: EditorVideoModel;
|
||||
videoAspectRatio?: string;
|
||||
videoResolution?: string;
|
||||
videoResolution?: '480p' | '720p';
|
||||
videoDurationSeconds?: 4 | 5;
|
||||
videoMode?: 'std';
|
||||
videoSound?: 'off';
|
||||
|
||||
@@ -489,7 +489,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
);
|
||||
fireEvent.click(infoButton);
|
||||
|
||||
const infoPanel = screen.getByRole('dialog', { name: '拼图素材图片信息' });
|
||||
const infoPanel = screen.getByRole('dialog', { name: '图片信息' });
|
||||
expect(within(infoPanel).getByText('图片类型')).toBeTruthy();
|
||||
expect(within(infoPanel).getByText('上传图片')).toBeTruthy();
|
||||
expect(within(infoPanel).getByText('生成输入')).toBeTruthy();
|
||||
@@ -547,9 +547,7 @@ describe('ImageCanvasEditorView', () => {
|
||||
fireEvent.click(
|
||||
screen.getAllByRole('button', { name: '查看旧布局图片图片信息' })[0]!,
|
||||
);
|
||||
const infoPanel = screen.getByRole('dialog', {
|
||||
name: '旧布局图片图片信息',
|
||||
});
|
||||
const infoPanel = screen.getByRole('dialog', { name: '图片信息' });
|
||||
expect(within(infoPanel).queryByText('Size')).toBeNull();
|
||||
expect(within(infoPanel).getByText('Resolution')).toBeTruthy();
|
||||
expect(within(infoPanel).getByText('1536 x 1024 px')).toBeTruthy();
|
||||
|
||||
@@ -49,6 +49,7 @@ export function ImageCanvasEditorView() {
|
||||
const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
||||
const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const characterReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const generationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const selectedLayerIdRef = useRef<string | null>(null);
|
||||
const selectedLayerIdsRef = useRef<string[]>([]);
|
||||
@@ -350,14 +351,17 @@ export function ImageCanvasEditorView() {
|
||||
layers,
|
||||
canvasSize,
|
||||
viewport,
|
||||
setViewport,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
generationReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
activeCanvasGenerationDialog,
|
||||
canvasGenerationDialogs,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogById,
|
||||
@@ -376,8 +380,11 @@ export function ImageCanvasEditorView() {
|
||||
quickEditPanel,
|
||||
setQuickEditPanel,
|
||||
setIsSpecMenuOpen,
|
||||
setIsGenerationReferenceMenuOpen,
|
||||
setIsCharacterSpecMenuOpen,
|
||||
setIsCharacterReferenceMenuOpen,
|
||||
isPickingGenerationReferenceFromCanvas,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
setIsPickingCharacterSpecFromCanvas,
|
||||
isPickingCharacterReferenceFromCanvas,
|
||||
@@ -385,12 +392,17 @@ export function ImageCanvasEditorView() {
|
||||
setIsIconSpecMenuOpen,
|
||||
isPickingIconSpecFromCanvas,
|
||||
setIsPickingIconSpecFromCanvas,
|
||||
setIsUiDesignSpecMenuOpen,
|
||||
isPickingUiDesignSpecFromCanvas,
|
||||
setIsPickingUiDesignSpecFromCanvas,
|
||||
openCharacterAnimationPanel,
|
||||
openEditDialog,
|
||||
openQuickEditPanel,
|
||||
pickCharacterSpecFromLayer,
|
||||
pickGenerationReferenceFromLayer,
|
||||
pickCharacterReferenceFromLayer,
|
||||
pickIconSpecFromLayer,
|
||||
pickUiDesignSpecFromLayer,
|
||||
hideGeneratedLayerPanelAfterBlur,
|
||||
clearDeletedLayerGenerationState,
|
||||
generationComposerStyle,
|
||||
@@ -482,12 +494,16 @@ export function ImageCanvasEditorView() {
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
isPickingGenerationReferenceFromCanvas,
|
||||
isPickingCharacterReferenceFromCanvas,
|
||||
isPickingIconSpecFromCanvas,
|
||||
isPickingUiDesignSpecFromCanvas,
|
||||
clearCanvasFocus,
|
||||
pickCharacterSpecFromLayer,
|
||||
pickGenerationReferenceFromLayer,
|
||||
pickCharacterReferenceFromLayer,
|
||||
pickIconSpecFromLayer,
|
||||
pickUiDesignSpecFromLayer,
|
||||
activateCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
moveViewportFromMinimapPointer,
|
||||
@@ -514,12 +530,16 @@ export function ImageCanvasEditorView() {
|
||||
setQuickEditPanel,
|
||||
closeEditorChromePanels,
|
||||
setIsSpecMenuOpen,
|
||||
setIsGenerationReferenceMenuOpen,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
setIsCharacterSpecMenuOpen,
|
||||
setIsCharacterReferenceMenuOpen,
|
||||
setIsPickingCharacterSpecFromCanvas,
|
||||
setIsPickingCharacterReferenceFromCanvas,
|
||||
setIsIconSpecMenuOpen,
|
||||
setIsPickingIconSpecFromCanvas,
|
||||
setIsUiDesignSpecMenuOpen,
|
||||
setIsPickingUiDesignSpecFromCanvas,
|
||||
setIsSpacePanning,
|
||||
setShiftPressed,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ImageIcon } from 'lucide-react';
|
||||
import {
|
||||
type CSSProperties,
|
||||
type Dispatch,
|
||||
@@ -11,6 +12,9 @@ import {
|
||||
PlatformFloatingMenu,
|
||||
PlatformFloatingMenuItem,
|
||||
} from '../common/PlatformFloatingMenu';
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformIconButton } from '../common/PlatformIconButton';
|
||||
import { PlatformTextField } from '../common/PlatformTextField';
|
||||
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
|
||||
import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAnimationPanelView';
|
||||
import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView';
|
||||
@@ -18,7 +22,10 @@ import { ImageCanvasEditGenerationModalView } from './ImageCanvasEditGenerationM
|
||||
import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSpritesheetComposerView';
|
||||
import { ImageCanvasQuickEditPanelView } from './ImageCanvasQuickEditPanelView';
|
||||
import { ImageCanvasSpecGenerationPanelView } from './ImageCanvasSpecGenerationPanelView';
|
||||
import { SPEC_TYPE_LABEL } from './ImageCanvasGenerationModel';
|
||||
import {
|
||||
SPEC_TYPE_LABEL,
|
||||
calculateEditorVideoPrice,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
import type {
|
||||
CharacterAnimationPanelState,
|
||||
CanvasLayer,
|
||||
@@ -33,14 +40,19 @@ type ImageCanvasGenerationComposerViewProps = {
|
||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
generationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
isSpecMenuOpen: boolean;
|
||||
isGenerationReferenceMenuOpen: boolean;
|
||||
isCharacterSpecMenuOpen: boolean;
|
||||
isCharacterReferenceMenuOpen: boolean;
|
||||
isIconSpecMenuOpen: boolean;
|
||||
isUiDesignSpecMenuOpen: boolean;
|
||||
isPickingGenerationReferenceFromCanvas: boolean;
|
||||
isPickingCharacterSpecFromCanvas: boolean;
|
||||
isPickingCharacterReferenceFromCanvas: boolean;
|
||||
isPickingIconSpecFromCanvas: boolean;
|
||||
isPickingUiDesignSpecFromCanvas: boolean;
|
||||
generateDialog: GenerateDialogState | null;
|
||||
generationComposerStyle: CSSProperties | null;
|
||||
iconComposerStyle: CSSProperties | null;
|
||||
@@ -58,12 +70,16 @@ type ImageCanvasGenerationComposerViewProps = {
|
||||
setCharacterAnimationPanel: Dispatch<
|
||||
SetStateAction<CharacterAnimationPanelState | null>
|
||||
>;
|
||||
setIsGenerationReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsCharacterSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsCharacterReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsUiDesignSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingGenerationReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingCharacterSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingCharacterReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingUiDesignSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
||||
onRequestUpload: (target: UploadTarget) => void;
|
||||
onSubmitImageGeneration: (dialog: GenerateDialogState) => void;
|
||||
@@ -116,18 +132,153 @@ function renderEditorPortal(node: ReactNode) {
|
||||
return createPortal(node, document.body);
|
||||
}
|
||||
|
||||
function ImageCanvasVideoGenerationComposerView({
|
||||
dialog,
|
||||
style,
|
||||
generationReferenceButtonRef,
|
||||
isGenerationReferenceMenuOpen,
|
||||
setGenerateDialog,
|
||||
setIsGenerationReferenceMenuOpen,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
onRequestUpload,
|
||||
onSubmit,
|
||||
}: {
|
||||
dialog: GenerateDialogState;
|
||||
style: CSSProperties;
|
||||
generationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
isGenerationReferenceMenuOpen: boolean;
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
setIsGenerationReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingGenerationReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
||||
onRequestUpload: (target: UploadTarget) => void;
|
||||
onSubmit: (dialog: GenerateDialogState) => void;
|
||||
}) {
|
||||
const resolution = dialog.videoResolution ?? '480p';
|
||||
const durationSeconds = dialog.videoDurationSeconds ?? 4;
|
||||
const price = calculateEditorVideoPrice(resolution, durationSeconds);
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="生成视频"
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
if (dialog.status !== 'generating') {
|
||||
onSubmit(dialog);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PlatformIconButton
|
||||
ref={generationReferenceButtonRef}
|
||||
variant="surfaceFloating"
|
||||
className="image-canvas-editor__generation-ref"
|
||||
label="添加视频参考图"
|
||||
disabled={dialog.status === 'generating'}
|
||||
onClick={() => setIsGenerationReferenceMenuOpen((open) => !open)}
|
||||
icon={<ImageIcon className="h-4 w-4" />}
|
||||
>
|
||||
<span>参考图</span>
|
||||
</PlatformIconButton>
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
aria-label="视频描述"
|
||||
value={dialog.prompt}
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder="描述视频画面"
|
||||
size="sm"
|
||||
density="compact"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) =>
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog
|
||||
? {
|
||||
...currentDialog,
|
||||
prompt: event.target.value,
|
||||
status:
|
||||
currentDialog.status === 'failed'
|
||||
? 'idle'
|
||||
: currentDialog.status,
|
||||
errorMessage:
|
||||
currentDialog.status === 'failed'
|
||||
? undefined
|
||||
: currentDialog.errorMessage,
|
||||
}
|
||||
: currentDialog,
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="image-canvas-editor__generation-composer-footer">
|
||||
<span className="image-canvas-editor__generation-ratio">
|
||||
16:9 · {resolution} · {durationSeconds}秒
|
||||
</span>
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
shape="pill"
|
||||
className="image-canvas-editor__generation-submit"
|
||||
disabled={dialog.status === 'generating'}
|
||||
aria-label="生成视频"
|
||||
>
|
||||
{dialog.status === 'generating' ? '生成中' : `${price}`}
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</form>
|
||||
{isGenerationReferenceMenuOpen
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
|
||||
label="参考图来源"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
generationReferenceButtonRef.current,
|
||||
'above',
|
||||
)}
|
||||
>
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen(false);
|
||||
setIsPickingGenerationReferenceFromCanvas(true);
|
||||
}}
|
||||
>
|
||||
从画布中选择
|
||||
</PlatformFloatingMenuItem>
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen(false);
|
||||
setIsPickingGenerationReferenceFromCanvas(false);
|
||||
onRequestUpload('generation-reference');
|
||||
}}
|
||||
>
|
||||
上传图片
|
||||
</PlatformFloatingMenuItem>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ImageCanvasGenerationComposerView({
|
||||
specToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
generationReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
isSpecMenuOpen,
|
||||
isGenerationReferenceMenuOpen,
|
||||
isCharacterSpecMenuOpen,
|
||||
isCharacterReferenceMenuOpen,
|
||||
isIconSpecMenuOpen,
|
||||
isUiDesignSpecMenuOpen,
|
||||
isPickingGenerationReferenceFromCanvas,
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
isPickingCharacterReferenceFromCanvas,
|
||||
isPickingIconSpecFromCanvas,
|
||||
isPickingUiDesignSpecFromCanvas,
|
||||
generateDialog,
|
||||
generationComposerStyle,
|
||||
iconComposerStyle,
|
||||
@@ -143,12 +294,16 @@ export function ImageCanvasGenerationComposerView({
|
||||
setGenerateDialog,
|
||||
setQuickEditPanel,
|
||||
setCharacterAnimationPanel,
|
||||
setIsGenerationReferenceMenuOpen,
|
||||
setIsCharacterSpecMenuOpen,
|
||||
setIsCharacterReferenceMenuOpen,
|
||||
setIsIconSpecMenuOpen,
|
||||
setIsUiDesignSpecMenuOpen,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
setIsPickingCharacterSpecFromCanvas,
|
||||
setIsPickingCharacterReferenceFromCanvas,
|
||||
setIsPickingIconSpecFromCanvas,
|
||||
setIsPickingUiDesignSpecFromCanvas,
|
||||
onOpenSpecDialog,
|
||||
onRequestUpload,
|
||||
onSubmitImageGeneration,
|
||||
@@ -192,7 +347,18 @@ export function ImageCanvasGenerationComposerView({
|
||||
dialog={generateDialog}
|
||||
style={generationComposerStyle}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
generationReferenceButtonRef={generationReferenceButtonRef}
|
||||
isGenerationReferenceMenuOpen={isGenerationReferenceMenuOpen}
|
||||
setIsGenerationReferenceMenuOpen={setIsGenerationReferenceMenuOpen}
|
||||
setIsPickingGenerationReferenceFromCanvas={
|
||||
setIsPickingGenerationReferenceFromCanvas
|
||||
}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
onRequestUpload={onRequestUpload}
|
||||
onToggleReferenceMenu={() =>
|
||||
setIsGenerationReferenceMenuOpen((open) => !open)
|
||||
}
|
||||
onSubmit={onSubmitImageGeneration}
|
||||
onClose={onCloseGenerateComposer}
|
||||
/>
|
||||
@@ -204,12 +370,58 @@ export function ImageCanvasGenerationComposerView({
|
||||
<ImageCanvasSpecGenerationPanelView
|
||||
dialog={generateDialog}
|
||||
style={generationComposerStyle}
|
||||
isGenerationReferenceMenuOpen={isGenerationReferenceMenuOpen}
|
||||
generationReferenceButtonRef={generationReferenceButtonRef}
|
||||
setIsGenerationReferenceMenuOpen={setIsGenerationReferenceMenuOpen}
|
||||
setIsPickingGenerationReferenceFromCanvas={
|
||||
setIsPickingGenerationReferenceFromCanvas
|
||||
}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
onUpdateSpecFormValue={onUpdateSpecFormValue}
|
||||
onRequestUpload={onRequestUpload}
|
||||
onSubmit={onSubmitImageGeneration}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{generateDialog?.mode === 'ui-design' &&
|
||||
generateDialog.composerOpen !== false &&
|
||||
generationComposerStyle ? (
|
||||
<ImageCanvasSpecGenerationPanelView
|
||||
dialog={generateDialog}
|
||||
style={generationComposerStyle}
|
||||
isGenerationReferenceMenuOpen={isUiDesignSpecMenuOpen}
|
||||
generationReferenceButtonRef={generationReferenceButtonRef}
|
||||
setIsGenerationReferenceMenuOpen={setIsUiDesignSpecMenuOpen}
|
||||
setIsPickingGenerationReferenceFromCanvas={
|
||||
setIsPickingUiDesignSpecFromCanvas
|
||||
}
|
||||
renderEditorPortal={renderEditorPortal}
|
||||
buildPortalMenuStyle={buildPortalMenuStyle}
|
||||
onUpdateSpecFormValue={onUpdateSpecFormValue}
|
||||
onRequestUpload={onRequestUpload}
|
||||
onSubmit={onSubmitImageGeneration}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{generateDialog?.mode === 'video' &&
|
||||
generateDialog.composerOpen !== false &&
|
||||
generationComposerStyle ? (
|
||||
<ImageCanvasVideoGenerationComposerView
|
||||
dialog={generateDialog}
|
||||
style={generationComposerStyle}
|
||||
generationReferenceButtonRef={generationReferenceButtonRef}
|
||||
isGenerationReferenceMenuOpen={isGenerationReferenceMenuOpen}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
setIsGenerationReferenceMenuOpen={setIsGenerationReferenceMenuOpen}
|
||||
setIsPickingGenerationReferenceFromCanvas={
|
||||
setIsPickingGenerationReferenceFromCanvas
|
||||
}
|
||||
onRequestUpload={onRequestUpload}
|
||||
onSubmit={onSubmitImageGeneration}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{generateDialog?.mode === 'character' && generationComposerStyle ? (
|
||||
<ImageCanvasCharacterGenerationComposerView
|
||||
dialog={generateDialog}
|
||||
@@ -268,11 +480,21 @@ export function ImageCanvasGenerationComposerView({
|
||||
请选择画布中的图片作为常规参考图,按 Esc 退出
|
||||
</div>
|
||||
) : null}
|
||||
{isPickingGenerationReferenceFromCanvas ? (
|
||||
<div className="image-canvas-editor__canvas-pick-hint">
|
||||
请选择画布中的图片作为参考图,按 Esc 退出
|
||||
</div>
|
||||
) : null}
|
||||
{isPickingIconSpecFromCanvas ? (
|
||||
<div className="image-canvas-editor__canvas-pick-hint">
|
||||
请选择画布中的图标素材规范,按 Esc 退出
|
||||
</div>
|
||||
) : null}
|
||||
{isPickingUiDesignSpecFromCanvas ? (
|
||||
<div className="image-canvas-editor__canvas-pick-hint">
|
||||
请选择画布中的图标素材规范,按 Esc 退出
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{quickEditPanel &&
|
||||
quickEditPanel.status !== 'generating' &&
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
ICON_FRAME_ORIGINAL_SIZE,
|
||||
SPEC_FRAME_DISPLAY_SIZE,
|
||||
SPEC_FRAME_ORIGINAL_SIZE,
|
||||
UI_DESIGN_FRAME_DISPLAY_SIZE,
|
||||
UI_DESIGN_FRAME_ORIGINAL_SIZE,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
|
||||
type CanvasSize = { width: number; height: number };
|
||||
@@ -180,6 +182,37 @@ export function createIconGenerationDialogDraft({
|
||||
};
|
||||
}
|
||||
|
||||
export function createUiDesignGenerationDialogDraft({
|
||||
canvasSize,
|
||||
viewport,
|
||||
imageModel,
|
||||
}: {
|
||||
canvasSize: CanvasSize;
|
||||
viewport: CanvasViewport;
|
||||
imageModel: string;
|
||||
}): Omit<CanvasGenerationDialogState, 'id'> {
|
||||
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
||||
const dimensionDefaults = resolveImageDimensionDefaults(imageModel);
|
||||
return {
|
||||
mode: 'ui-design',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
uiDesignSpecReference: null,
|
||||
imageModel,
|
||||
aspectRatio: dimensionDefaults.aspectRatio,
|
||||
imageSize: dimensionDefaults.imageSize,
|
||||
placeholder: {
|
||||
x: worldCenter.x - UI_DESIGN_FRAME_DISPLAY_SIZE.width / 2,
|
||||
y: worldCenter.y - UI_DESIGN_FRAME_DISPLAY_SIZE.height / 2,
|
||||
width: UI_DESIGN_FRAME_DISPLAY_SIZE.width,
|
||||
height: UI_DESIGN_FRAME_DISPLAY_SIZE.height,
|
||||
originalWidth: UI_DESIGN_FRAME_ORIGINAL_SIZE.width,
|
||||
originalHeight: UI_DESIGN_FRAME_ORIGINAL_SIZE.height,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createEditDialogDraft(
|
||||
sourceLayer: CanvasLayer,
|
||||
): GenerateDialogState {
|
||||
@@ -255,6 +288,24 @@ export function appendCharacterReference(
|
||||
: dialog;
|
||||
}
|
||||
|
||||
export function appendGenerationReference(
|
||||
dialog: GenerateDialogState | null,
|
||||
layer: CanvasLayer,
|
||||
): GenerateDialogState | null {
|
||||
return dialog?.mode === 'generate' ||
|
||||
dialog?.mode === 'video' ||
|
||||
dialog?.mode === 'spec'
|
||||
? {
|
||||
...resetFailedGenerationDialog(dialog),
|
||||
generationReferences: [
|
||||
...(dialog.generationReferences ?? []),
|
||||
createCanvasLayerReference(layer),
|
||||
],
|
||||
composerOpen: true,
|
||||
}
|
||||
: dialog;
|
||||
}
|
||||
|
||||
export function assignIconSpecReference(
|
||||
dialog: GenerateDialogState | null,
|
||||
layer: CanvasLayer,
|
||||
@@ -271,6 +322,19 @@ export function assignIconSpecReference(
|
||||
: dialog;
|
||||
}
|
||||
|
||||
export function assignUiDesignSpecReference(
|
||||
dialog: GenerateDialogState | null,
|
||||
layer: CanvasLayer,
|
||||
): GenerateDialogState | null {
|
||||
return dialog?.mode === 'ui-design'
|
||||
? {
|
||||
...resetFailedGenerationDialog(dialog),
|
||||
uiDesignSpecReference: createCanvasLayerReference(layer),
|
||||
composerOpen: true,
|
||||
}
|
||||
: dialog;
|
||||
}
|
||||
|
||||
export function updateSpecFormDialogValue(
|
||||
dialog: GenerateDialogState | null,
|
||||
key: keyof SpecFormValues,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type {
|
||||
EditorCharacterAnimationGenerationInput,
|
||||
EditorIconSpritesheetGenerationInput,
|
||||
EditorImageGenerationInput,
|
||||
EditorVideoGenerationInput,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import type {
|
||||
CanvasGenerationInputs,
|
||||
@@ -22,7 +23,10 @@ import {
|
||||
buildImageGenerationInputs,
|
||||
buildSpecGenerationInputs,
|
||||
buildSpecPrompt,
|
||||
buildUiDesignGenerationInputs,
|
||||
buildVideoGenerationInputs,
|
||||
calculateCharacterAnimationPrice,
|
||||
calculateEditorVideoPrice,
|
||||
resolveCharacterAnimationSourceImageSrc,
|
||||
} from './ImageCanvasGenerationModel';
|
||||
|
||||
@@ -49,6 +53,15 @@ export type ImageGenerationSubmissionPlan =
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
};
|
||||
rememberImageModel?: string;
|
||||
}
|
||||
| {
|
||||
kind: 'video';
|
||||
normalizedPrompt: string;
|
||||
input: EditorVideoGenerationInput;
|
||||
result: {
|
||||
title: string;
|
||||
generationInputs: CanvasGenerationInputs;
|
||||
};
|
||||
};
|
||||
|
||||
export type IconSpritesheetGenerationSubmissionPlan =
|
||||
@@ -156,14 +169,82 @@ export function buildImageGenerationSubmissionPlan({
|
||||
};
|
||||
}
|
||||
|
||||
if (dialog.mode === 'ui-design') {
|
||||
const imageModel = dialog.imageModel ?? DEFAULT_IMAGE_MODEL;
|
||||
const referenceImageSrcs = [dialog.uiDesignSpecReference?.src].filter(
|
||||
(src): src is string => Boolean(src),
|
||||
);
|
||||
return {
|
||||
kind: 'image',
|
||||
normalizedPrompt,
|
||||
input: {
|
||||
prompt: normalizedPrompt,
|
||||
kind: 'ui-design',
|
||||
model: imageModel,
|
||||
aspectRatio: dialog.aspectRatio ?? '16:9',
|
||||
imageSize: dialog.imageSize ?? '1K',
|
||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||
},
|
||||
result: {
|
||||
generationInputs: buildImageGenerationInputs(normalizedPrompt),
|
||||
assetKind: 'ui-design',
|
||||
title: `UI设计图 ${nextGeneratedIndex}`,
|
||||
generationInputs: buildUiDesignGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.uiDesignSpecReference,
|
||||
),
|
||||
},
|
||||
rememberImageModel: imageModel,
|
||||
};
|
||||
}
|
||||
|
||||
if (dialog.mode === 'video') {
|
||||
const resolution = dialog.videoResolution ?? '480p';
|
||||
const durationSeconds = dialog.videoDurationSeconds ?? 4;
|
||||
const model = dialog.videoModel ?? 'seedance2.0';
|
||||
return {
|
||||
kind: 'video',
|
||||
normalizedPrompt,
|
||||
input: {
|
||||
prompt: normalizedPrompt,
|
||||
model,
|
||||
aspectRatio: '16:9',
|
||||
durationSeconds,
|
||||
resolution,
|
||||
mode: 'std',
|
||||
sound: 'off',
|
||||
priceMudPoints: calculateEditorVideoPrice(
|
||||
resolution,
|
||||
durationSeconds,
|
||||
),
|
||||
},
|
||||
result: {
|
||||
title: `生成视频 ${nextGeneratedIndex}`,
|
||||
generationInputs: buildVideoGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.generationReferences,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
kind: 'image',
|
||||
normalizedPrompt,
|
||||
input: {
|
||||
prompt: normalizedPrompt,
|
||||
...(dialog.generationReferences?.length
|
||||
? {
|
||||
referenceImageSrcs: dialog.generationReferences.map(
|
||||
(reference) => reference.src,
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
result: {
|
||||
generationInputs: buildImageGenerationInputs(
|
||||
normalizedPrompt,
|
||||
dialog.generationReferences,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { createRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { GenerateDialogState } from './ImageCanvasEditorTypes';
|
||||
import type {
|
||||
GenerateDialogState,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSpritesheetComposerView';
|
||||
|
||||
function createIconDialog(
|
||||
@@ -35,7 +38,7 @@ function IconComposerHarness({
|
||||
initialDialog: GenerateDialogState;
|
||||
initialMenuOpen?: boolean;
|
||||
onOpenSpecDialog?: (specType: 'character' | 'ui' | 'icon' | 'custom') => void;
|
||||
onRequestUpload?: (target: 'asset' | 'spec-reference' | 'character-spec' | 'character-reference' | 'icon-spec') => void;
|
||||
onRequestUpload?: (target: UploadTarget) => void;
|
||||
onUpdateIconDescription?: (index: number, value: string) => void;
|
||||
onAddIconDescription?: () => void;
|
||||
onRememberImageModel?: (model: string) => void;
|
||||
|
||||
@@ -109,7 +109,7 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
cleanup();
|
||||
renderPanel({ dialog: createSpecDialog({ specType: 'icon' }) });
|
||||
|
||||
expect(screen.queryByText('添加参考图')).toBeNull();
|
||||
expect(screen.getByRole('button', { name: '参考图' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('requests reference upload and submits while idle', () => {
|
||||
@@ -122,7 +122,7 @@ describe('ImageCanvasSpecGenerationPanelView', () => {
|
||||
onSubmit: submitSpec,
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加参考图' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '参考图' }));
|
||||
fireEvent.click(screen.getByRole('button', { name: '提交生成规范' }));
|
||||
|
||||
expect(requestUpload).toHaveBeenCalledWith('spec-reference');
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { type CSSProperties } from 'react';
|
||||
import {
|
||||
type CSSProperties,
|
||||
type Dispatch,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
} from 'react';
|
||||
import { ImagePlus } from 'lucide-react';
|
||||
|
||||
import { PlatformActionButton } from '../common/PlatformActionButton';
|
||||
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
||||
import {
|
||||
PlatformFloatingMenu,
|
||||
PlatformFloatingMenuItem,
|
||||
} from '../common/PlatformFloatingMenu';
|
||||
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
||||
import {
|
||||
PlatformSelectField,
|
||||
@@ -21,6 +31,15 @@ import type {
|
||||
type ImageCanvasSpecGenerationPanelViewProps = {
|
||||
dialog: GenerateDialogState;
|
||||
style: CSSProperties;
|
||||
isGenerationReferenceMenuOpen?: boolean;
|
||||
generationReferenceButtonRef?: RefObject<HTMLButtonElement | null>;
|
||||
setIsGenerationReferenceMenuOpen?: Dispatch<SetStateAction<boolean>>;
|
||||
setIsPickingGenerationReferenceFromCanvas?: Dispatch<SetStateAction<boolean>>;
|
||||
renderEditorPortal?: (node: ReactNode) => ReactNode;
|
||||
buildPortalMenuStyle?: (
|
||||
anchor: HTMLElement | null,
|
||||
placement: 'above' | 'below',
|
||||
) => CSSProperties;
|
||||
onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void;
|
||||
onRequestUpload: (target: UploadTarget) => void;
|
||||
onSubmit: (dialog: GenerateDialogState) => void;
|
||||
@@ -29,16 +48,39 @@ type ImageCanvasSpecGenerationPanelViewProps = {
|
||||
export function ImageCanvasSpecGenerationPanelView({
|
||||
dialog,
|
||||
style,
|
||||
isGenerationReferenceMenuOpen = false,
|
||||
generationReferenceButtonRef,
|
||||
setIsGenerationReferenceMenuOpen,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
renderEditorPortal = (node) => node,
|
||||
buildPortalMenuStyle = () => ({}),
|
||||
onUpdateSpecFormValue,
|
||||
onRequestUpload,
|
||||
onSubmit,
|
||||
}: ImageCanvasSpecGenerationPanelViewProps) {
|
||||
const isUiDesignDialog = dialog.mode === 'ui-design';
|
||||
const referenceLabel = isUiDesignDialog ? 'UI设计图标素材规范' : '参考图';
|
||||
const uploadTarget: UploadTarget = isUiDesignDialog
|
||||
? 'ui-design-icon-spec'
|
||||
: 'spec-reference';
|
||||
const reference = isUiDesignDialog
|
||||
? dialog.uiDesignSpecReference
|
||||
: dialog.specReference;
|
||||
const openReferenceMenu = () => {
|
||||
if (setIsGenerationReferenceMenuOpen) {
|
||||
setIsGenerationReferenceMenuOpen((open) => !open);
|
||||
return;
|
||||
}
|
||||
onRequestUpload(uploadTarget);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
className="image-canvas-editor__generation-composer image-canvas-editor__spec-composer"
|
||||
className="image-canvas-editor__generation-composer image-canvas-editor__generation-composer--image image-canvas-editor__spec-composer"
|
||||
style={style}
|
||||
role="dialog"
|
||||
aria-label="生成规范"
|
||||
aria-label={isUiDesignDialog ? '生成UI设计图' : '生成规范'}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
@@ -48,7 +90,29 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
}}
|
||||
>
|
||||
<div className="image-canvas-editor__spec-fields">
|
||||
{dialog.specType === 'custom' ? (
|
||||
{isUiDesignDialog ? (
|
||||
<label className="image-canvas-editor__field-block">
|
||||
<PlatformFieldLabel
|
||||
variant="form"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
UI设计要求
|
||||
</PlatformFieldLabel>
|
||||
<PlatformTextField
|
||||
variant="textarea"
|
||||
aria-label="UI设计要求"
|
||||
value={dialog.prompt}
|
||||
disabled={dialog.status === 'generating'}
|
||||
placeholder="描述要生成的UI界面"
|
||||
size="sm"
|
||||
density="compact"
|
||||
className="image-canvas-editor__generation-prompt"
|
||||
onChange={(event) =>
|
||||
onUpdateSpecFormValue('customPrompt', event.target.value)
|
||||
}
|
||||
/>
|
||||
</label>
|
||||
) : dialog.specType === 'custom' ? (
|
||||
<label className="image-canvas-editor__field-block">
|
||||
<PlatformFieldLabel
|
||||
variant="form"
|
||||
@@ -165,29 +229,31 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
{dialog.specType !== 'icon' ? (
|
||||
<div className="image-canvas-editor__field-block">
|
||||
{isUiDesignDialog || dialog.specType ? (
|
||||
<div className="image-canvas-editor__field-block image-canvas-editor__generation-ref">
|
||||
<PlatformFieldLabel
|
||||
variant="form"
|
||||
className="image-canvas-editor__field-title"
|
||||
>
|
||||
参考图
|
||||
{referenceLabel}
|
||||
</PlatformFieldLabel>
|
||||
<button
|
||||
ref={generationReferenceButtonRef}
|
||||
type="button"
|
||||
className="image-canvas-editor__character-spec-ref image-canvas-editor__reference-tile image-canvas-editor__reference-tile--spec"
|
||||
disabled={dialog.status === 'generating'}
|
||||
onClick={() => onRequestUpload('spec-reference')}
|
||||
onClick={openReferenceMenu}
|
||||
aria-label={referenceLabel}
|
||||
>
|
||||
<span className="image-canvas-editor__reference-tile-visual">
|
||||
{dialog.specReference ? (
|
||||
<img src={dialog.specReference.src} alt="" aria-hidden="true" />
|
||||
{reference ? (
|
||||
<img src={reference.src} alt="" aria-hidden="true" />
|
||||
) : (
|
||||
<ImagePlus className="h-4 w-4" aria-hidden="true" />
|
||||
)}
|
||||
</span>
|
||||
<span className="image-canvas-editor__reference-tile-copy">
|
||||
{dialog.specReference?.label ?? '添加参考图'}
|
||||
{reference?.label ?? '添加参考图'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
@@ -204,14 +270,14 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
{dialog.errorMessage}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
<div className="image-canvas-editor__spec-footer">
|
||||
<div className="image-canvas-editor__generation-composer-footer image-canvas-editor__spec-footer">
|
||||
<PlatformActionButton
|
||||
type="submit"
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
className="image-canvas-editor__spec-submit"
|
||||
className="image-canvas-editor__generation-submit image-canvas-editor__spec-submit"
|
||||
disabled={dialog.status === 'generating'}
|
||||
aria-label="提交生成规范"
|
||||
aria-label={isUiDesignDialog ? '生成UI设计图' : '提交生成规范'}
|
||||
>
|
||||
{dialog.status === 'generating'
|
||||
? '生成中'
|
||||
@@ -219,5 +285,37 @@ export function ImageCanvasSpecGenerationPanelView({
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</form>
|
||||
{isGenerationReferenceMenuOpen && generationReferenceButtonRef
|
||||
? renderEditorPortal(
|
||||
<PlatformFloatingMenu
|
||||
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
|
||||
label="参考图来源"
|
||||
placement="top-start"
|
||||
style={buildPortalMenuStyle(
|
||||
generationReferenceButtonRef.current,
|
||||
'above',
|
||||
)}
|
||||
>
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen?.(false);
|
||||
setIsPickingGenerationReferenceFromCanvas?.(true);
|
||||
}}
|
||||
>
|
||||
从画布中选择
|
||||
</PlatformFloatingMenuItem>
|
||||
<PlatformFloatingMenuItem
|
||||
onClick={() => {
|
||||
setIsGenerationReferenceMenuOpen?.(false);
|
||||
setIsPickingGenerationReferenceFromCanvas?.(false);
|
||||
onRequestUpload(uploadTarget);
|
||||
}}
|
||||
>
|
||||
上传图片
|
||||
</PlatformFloatingMenuItem>
|
||||
</PlatformFloatingMenu>,
|
||||
)
|
||||
: null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ type PersistedUploadAsset = {
|
||||
type GenerationReferenceUploadTarget =
|
||||
| 'character-reference'
|
||||
| 'character-spec'
|
||||
| 'generation-reference'
|
||||
| 'icon-spec'
|
||||
| 'spec-reference';
|
||||
| 'spec-reference'
|
||||
| 'ui-design-icon-spec';
|
||||
|
||||
export const UPLOAD_LAYER_FALLBACK_SIZE = {
|
||||
width: 420,
|
||||
@@ -114,6 +116,27 @@ export function applyGenerationReferenceUpload({
|
||||
}
|
||||
: dialog;
|
||||
}
|
||||
if (target === 'ui-design-icon-spec') {
|
||||
return dialog?.mode === 'ui-design'
|
||||
? {
|
||||
...setFailedGenerationIdle(dialog),
|
||||
uiDesignSpecReference: firstReference,
|
||||
}
|
||||
: dialog;
|
||||
}
|
||||
if (target === 'generation-reference') {
|
||||
return dialog?.mode === 'generate' ||
|
||||
dialog?.mode === 'video' ||
|
||||
dialog?.mode === 'spec'
|
||||
? {
|
||||
...setFailedGenerationIdle(dialog),
|
||||
generationReferences: [
|
||||
...(dialog.generationReferences ?? []),
|
||||
...references,
|
||||
],
|
||||
}
|
||||
: dialog;
|
||||
}
|
||||
return dialog?.mode === 'character'
|
||||
? {
|
||||
...setFailedGenerationIdle(dialog),
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
generateEditorCharacterAnimation,
|
||||
generateEditorIconSpritesheet,
|
||||
generateEditorImage,
|
||||
generateEditorVideo,
|
||||
} from '../../services/image-editor/editorProjectClient';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
createGeneratedResultLayer,
|
||||
createIconSpritesheetResultLayers,
|
||||
createQuickEditResultLayer,
|
||||
createVideoResultLayer,
|
||||
} from './ImageCanvasGenerationLayerModel';
|
||||
import {
|
||||
buildEditGenerationInputs,
|
||||
@@ -242,6 +244,53 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
],
|
||||
);
|
||||
|
||||
const addVideoResultLayer = useCallback(
|
||||
(
|
||||
generated: Parameters<typeof createVideoResultLayer>[0]['generated'],
|
||||
title: string,
|
||||
generationInputs: CanvasGenerationInputs,
|
||||
frame?: GenerateDialogState['placeholder'],
|
||||
dialogId?: string,
|
||||
) => {
|
||||
layerCounterRef.current += 1;
|
||||
const generatedIndex = layerCounterRef.current;
|
||||
const nextLayer = createVideoResultLayer({
|
||||
generated,
|
||||
generatedIndex,
|
||||
title,
|
||||
canvasSize,
|
||||
viewport,
|
||||
generationInputs,
|
||||
frame,
|
||||
});
|
||||
|
||||
appendCanvasLayersWithResources([nextLayer]);
|
||||
selectSingleLayer(nextLayer.id);
|
||||
setActiveSidebarPanel('layers');
|
||||
if (dialogId) {
|
||||
updateCanvasGenerationDialogById(dialogId, (currentDialog) => ({
|
||||
...currentDialog,
|
||||
status: 'idle',
|
||||
composerOpen: true,
|
||||
generatedLayerId: nextLayer.id,
|
||||
placeholder: undefined,
|
||||
errorMessage: undefined,
|
||||
}));
|
||||
}
|
||||
setActiveTool('select');
|
||||
},
|
||||
[
|
||||
appendCanvasLayersWithResources,
|
||||
canvasSize,
|
||||
layerCounterRef,
|
||||
selectSingleLayer,
|
||||
setActiveSidebarPanel,
|
||||
setActiveTool,
|
||||
updateCanvasGenerationDialogById,
|
||||
viewport,
|
||||
],
|
||||
);
|
||||
|
||||
const submitIconSpritesheetGeneration = useCallback(
|
||||
async (dialog: GenerateDialogState) => {
|
||||
if (dialog.mode !== 'icon') {
|
||||
@@ -397,6 +446,15 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
sourceLayer: submissionPlan.sourceLayer,
|
||||
generationInputs: submissionPlan.generationInputs,
|
||||
});
|
||||
} else if (submissionPlan.kind === 'video') {
|
||||
const generated = await generateEditorVideo(submissionPlan.input);
|
||||
addVideoResultLayer(
|
||||
generated,
|
||||
submissionPlan.result.title,
|
||||
submissionPlan.result.generationInputs,
|
||||
getGeneratingDialogPlaceholder(dialog),
|
||||
canvasDialog?.id,
|
||||
);
|
||||
} else {
|
||||
const generated = await generateEditorImage(submissionPlan.input);
|
||||
if (submissionPlan.rememberImageModel) {
|
||||
@@ -432,6 +490,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({
|
||||
},
|
||||
[
|
||||
addGeneratedResultLayer,
|
||||
addVideoResultLayer,
|
||||
getGeneratingDialogPlaceholder,
|
||||
layerCounterRef,
|
||||
layers,
|
||||
|
||||
@@ -63,6 +63,8 @@ function GenerationSurfaceHarness() {
|
||||
const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const characterReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const generationReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [viewport, setViewport] = useState({ x: 10, y: 20, scale: 2 });
|
||||
const dialogs = useCanvasGenerationDialogs();
|
||||
const activeDialog = dialogs.generateDialog;
|
||||
const activeCanvasDialog =
|
||||
@@ -72,15 +74,18 @@ function GenerationSurfaceHarness() {
|
||||
const surface = useImageCanvasGenerationSurface({
|
||||
layers,
|
||||
canvasSize: { width: 900, height: 640 },
|
||||
viewport: { x: 10, y: 20, scale: 2 },
|
||||
viewport,
|
||||
setViewport,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
generationReferenceButtonRef,
|
||||
generateDialog: dialogs.generateDialog,
|
||||
setGenerateDialog: dialogs.setGenerateDialog,
|
||||
activeCanvasGenerationDialog: activeCanvasDialog,
|
||||
canvasGenerationDialogs: dialogs.canvasGenerationDialogs,
|
||||
openCanvasGenerationDialog: dialogs.openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogById: dialogs.removeCanvasGenerationDialogById,
|
||||
|
||||
@@ -34,14 +34,17 @@ type ImageCanvasGenerationSurfaceOptions = {
|
||||
layers: CanvasLayer[];
|
||||
canvasSize: { width: number; height: number };
|
||||
viewport: CanvasViewport;
|
||||
setViewport: Dispatch<SetStateAction<CanvasViewport>>;
|
||||
layerCounterRef: MutableRefObject<number>;
|
||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
generationReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
generateDialog: GenerateDialogState | null;
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
activeCanvasGenerationDialog: CanvasGenerationDialogState | null;
|
||||
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
||||
openCanvasGenerationDialog: (
|
||||
dialog: Omit<CanvasGenerationDialogState, 'id'>,
|
||||
) => void;
|
||||
@@ -68,14 +71,17 @@ export function useImageCanvasGenerationSurface({
|
||||
layers,
|
||||
canvasSize,
|
||||
viewport,
|
||||
setViewport,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
generationReferenceButtonRef,
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
activeCanvasGenerationDialog,
|
||||
canvasGenerationDialogs,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogById,
|
||||
@@ -94,6 +100,8 @@ export function useImageCanvasGenerationSurface({
|
||||
layers,
|
||||
canvasSize,
|
||||
viewport,
|
||||
setViewport,
|
||||
canvasGenerationDialogs,
|
||||
layerCounterRef,
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
@@ -173,13 +181,21 @@ export function useImageCanvasGenerationSurface({
|
||||
specToolWrapRef={specToolWrapRef}
|
||||
characterSpecButtonRef={characterSpecButtonRef}
|
||||
characterReferenceButtonRef={characterReferenceButtonRef}
|
||||
generationReferenceButtonRef={generationReferenceButtonRef}
|
||||
iconSpecButtonRef={iconSpecButtonRef}
|
||||
isSpecMenuOpen={generationWorkflow.isSpecMenuOpen}
|
||||
isGenerationReferenceMenuOpen={
|
||||
generationWorkflow.isGenerationReferenceMenuOpen
|
||||
}
|
||||
isCharacterSpecMenuOpen={generationWorkflow.isCharacterSpecMenuOpen}
|
||||
isCharacterReferenceMenuOpen={
|
||||
generationWorkflow.isCharacterReferenceMenuOpen
|
||||
}
|
||||
isIconSpecMenuOpen={generationWorkflow.isIconSpecMenuOpen}
|
||||
isUiDesignSpecMenuOpen={generationWorkflow.isUiDesignSpecMenuOpen}
|
||||
isPickingGenerationReferenceFromCanvas={
|
||||
generationWorkflow.isPickingGenerationReferenceFromCanvas
|
||||
}
|
||||
isPickingCharacterSpecFromCanvas={
|
||||
generationWorkflow.isPickingCharacterSpecFromCanvas
|
||||
}
|
||||
@@ -189,6 +205,9 @@ export function useImageCanvasGenerationSurface({
|
||||
isPickingIconSpecFromCanvas={
|
||||
generationWorkflow.isPickingIconSpecFromCanvas
|
||||
}
|
||||
isPickingUiDesignSpecFromCanvas={
|
||||
generationWorkflow.isPickingUiDesignSpecFromCanvas
|
||||
}
|
||||
generateDialog={generateDialog}
|
||||
generationComposerStyle={generationComposerStyle}
|
||||
iconComposerStyle={iconComposerStyle}
|
||||
@@ -214,7 +233,16 @@ export function useImageCanvasGenerationSurface({
|
||||
setIsCharacterReferenceMenuOpen={
|
||||
generationWorkflow.setIsCharacterReferenceMenuOpen
|
||||
}
|
||||
setIsGenerationReferenceMenuOpen={
|
||||
generationWorkflow.setIsGenerationReferenceMenuOpen
|
||||
}
|
||||
setIsIconSpecMenuOpen={generationWorkflow.setIsIconSpecMenuOpen}
|
||||
setIsUiDesignSpecMenuOpen={
|
||||
generationWorkflow.setIsUiDesignSpecMenuOpen
|
||||
}
|
||||
setIsPickingGenerationReferenceFromCanvas={
|
||||
generationWorkflow.setIsPickingGenerationReferenceFromCanvas
|
||||
}
|
||||
setIsPickingCharacterSpecFromCanvas={
|
||||
generationWorkflow.setIsPickingCharacterSpecFromCanvas
|
||||
}
|
||||
@@ -224,6 +252,9 @@ export function useImageCanvasGenerationSurface({
|
||||
setIsPickingIconSpecFromCanvas={
|
||||
generationWorkflow.setIsPickingIconSpecFromCanvas
|
||||
}
|
||||
setIsPickingUiDesignSpecFromCanvas={
|
||||
generationWorkflow.setIsPickingUiDesignSpecFromCanvas
|
||||
}
|
||||
onOpenSpecDialog={generationWorkflow.openSpecDialog}
|
||||
onRequestUpload={requestUpload}
|
||||
onSubmitImageGeneration={(dialog) =>
|
||||
|
||||
@@ -426,7 +426,7 @@ describe('useImageCanvasGenerationWorkflow', () => {
|
||||
expect.objectContaining({
|
||||
size: '2048x1152',
|
||||
kind: 'spec',
|
||||
model: 'gpt-image-2',
|
||||
model: 'gemini-3.1-flash-image-preview',
|
||||
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||
prompt: expect.stringContaining('参考图生成规范'),
|
||||
}),
|
||||
|
||||
@@ -22,7 +22,9 @@ import type {
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
appendCharacterReference,
|
||||
appendGenerationReference,
|
||||
appendIconDescriptionToDialog,
|
||||
assignUiDesignSpecReference,
|
||||
assignCharacterSpecReference,
|
||||
assignIconSpecReference,
|
||||
closeGenerateComposerDialog,
|
||||
@@ -38,6 +40,10 @@ import {
|
||||
updateIconDescriptionInDialog,
|
||||
updateSpecFormDialogValue,
|
||||
} from './ImageCanvasGenerationDialogModel';
|
||||
import {
|
||||
centerViewportOnPlacement,
|
||||
chooseGenerationPlacement,
|
||||
} from './ImageCanvasGenerationPlacementModel';
|
||||
import {
|
||||
buildQuickEditModelOptions,
|
||||
buildQuickEditSizeOptions,
|
||||
@@ -57,6 +63,8 @@ type GenerationWorkflowOptions = {
|
||||
layers: CanvasLayer[];
|
||||
canvasSize: CanvasSize;
|
||||
viewport: CanvasViewport;
|
||||
setViewport: Dispatch<SetStateAction<CanvasViewport>>;
|
||||
canvasGenerationDialogs: CanvasGenerationDialogState[];
|
||||
layerCounterRef: MutableRefObject<number>;
|
||||
generateDialog: GenerateDialogState | null;
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
@@ -85,6 +93,8 @@ export function useImageCanvasGenerationWorkflow({
|
||||
layers,
|
||||
canvasSize,
|
||||
viewport,
|
||||
setViewport,
|
||||
canvasGenerationDialogs,
|
||||
layerCounterRef,
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
@@ -102,9 +112,15 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setImageContextMenu,
|
||||
}: GenerationWorkflowOptions) {
|
||||
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
|
||||
const [isGenerationReferenceMenuOpen, setIsGenerationReferenceMenuOpen] =
|
||||
useState(false);
|
||||
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(false);
|
||||
const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] =
|
||||
useState(false);
|
||||
const [
|
||||
isPickingGenerationReferenceFromCanvas,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
] = useState(false);
|
||||
const [
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
setIsPickingCharacterSpecFromCanvas,
|
||||
@@ -116,6 +132,9 @@ export function useImageCanvasGenerationWorkflow({
|
||||
const [isIconSpecMenuOpen, setIsIconSpecMenuOpen] = useState(false);
|
||||
const [isPickingIconSpecFromCanvas, setIsPickingIconSpecFromCanvas] =
|
||||
useState(false);
|
||||
const [isUiDesignSpecMenuOpen, setIsUiDesignSpecMenuOpen] = useState(false);
|
||||
const [isPickingUiDesignSpecFromCanvas, setIsPickingUiDesignSpecFromCanvas] =
|
||||
useState(false);
|
||||
const [quickEditPanel, setQuickEditPanel] =
|
||||
useState<QuickEditPanelState | null>(null);
|
||||
const [characterAnimationPanel, setCharacterAnimationPanel] =
|
||||
@@ -153,17 +172,40 @@ export function useImageCanvasGenerationWorkflow({
|
||||
: DEFAULT_ICON_DESCRIPTIONS;
|
||||
|
||||
const openGenerateDialog = useCallback(() => {
|
||||
openCanvasGenerationDialog(
|
||||
createGenerateDialogDraft({ canvasSize, viewport }),
|
||||
const draft = createGenerateDialogDraft({ canvasSize, viewport });
|
||||
const draftPlaceholder = draft.placeholder;
|
||||
if (!draftPlaceholder) {
|
||||
return;
|
||||
}
|
||||
const placement = chooseGenerationPlacement({
|
||||
canvasSize,
|
||||
viewport,
|
||||
frame: draftPlaceholder,
|
||||
layers,
|
||||
generationDialogs: canvasGenerationDialogs,
|
||||
}) ?? draftPlaceholder;
|
||||
openCanvasGenerationDialog({
|
||||
...draft,
|
||||
placeholder: placement,
|
||||
});
|
||||
setViewport(
|
||||
centerViewportOnPlacement({
|
||||
canvasSize,
|
||||
viewport,
|
||||
placement,
|
||||
}),
|
||||
);
|
||||
setActiveTool('generate');
|
||||
selectSingleLayer(null);
|
||||
setQuickEditPanel(null);
|
||||
}, [
|
||||
canvasSize,
|
||||
canvasGenerationDialogs,
|
||||
layers,
|
||||
openCanvasGenerationDialog,
|
||||
selectSingleLayer,
|
||||
setActiveTool,
|
||||
setViewport,
|
||||
viewport,
|
||||
]);
|
||||
|
||||
@@ -202,9 +244,13 @@ export function useImageCanvasGenerationWorkflow({
|
||||
|
||||
const openCharacterGenerationDialog = useCallback(() => {
|
||||
setIsSpecMenuOpen(false);
|
||||
setIsGenerationReferenceMenuOpen(false);
|
||||
setIsCharacterReferenceMenuOpen(false);
|
||||
setIsPickingGenerationReferenceFromCanvas(false);
|
||||
setIsPickingCharacterSpecFromCanvas(false);
|
||||
setIsPickingCharacterReferenceFromCanvas(false);
|
||||
setIsUiDesignSpecMenuOpen(false);
|
||||
setIsPickingUiDesignSpecFromCanvas(false);
|
||||
openCanvasGenerationDialog(
|
||||
createCharacterGenerationDialogDraft({
|
||||
canvasSize,
|
||||
@@ -226,9 +272,13 @@ export function useImageCanvasGenerationWorkflow({
|
||||
|
||||
const openIconGenerationDialog = useCallback(() => {
|
||||
setIsSpecMenuOpen(false);
|
||||
setIsGenerationReferenceMenuOpen(false);
|
||||
setIsCharacterReferenceMenuOpen(false);
|
||||
setIsPickingGenerationReferenceFromCanvas(false);
|
||||
setIsPickingCharacterSpecFromCanvas(false);
|
||||
setIsPickingCharacterReferenceFromCanvas(false);
|
||||
setIsUiDesignSpecMenuOpen(false);
|
||||
setIsPickingUiDesignSpecFromCanvas(false);
|
||||
setIsPickingIconSpecFromCanvas(false);
|
||||
openCanvasGenerationDialog(
|
||||
createIconGenerationDialogDraft({
|
||||
@@ -292,6 +342,18 @@ export function useImageCanvasGenerationWorkflow({
|
||||
[setGenerateDialog, setImageContextMenu],
|
||||
);
|
||||
|
||||
const pickGenerationReferenceFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
appendGenerationReference(currentDialog, layer),
|
||||
);
|
||||
setIsPickingGenerationReferenceFromCanvas(false);
|
||||
setIsGenerationReferenceMenuOpen(false);
|
||||
setImageContextMenu(null);
|
||||
},
|
||||
[setGenerateDialog, setImageContextMenu],
|
||||
);
|
||||
|
||||
const pickCharacterReferenceFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
@@ -318,6 +380,18 @@ export function useImageCanvasGenerationWorkflow({
|
||||
[setGenerateDialog, setImageContextMenu],
|
||||
);
|
||||
|
||||
const pickUiDesignSpecFromLayer = useCallback(
|
||||
(layer: CanvasLayer) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
assignUiDesignSpecReference(currentDialog, layer),
|
||||
);
|
||||
setIsPickingUiDesignSpecFromCanvas(false);
|
||||
setIsUiDesignSpecMenuOpen(false);
|
||||
setImageContextMenu(null);
|
||||
},
|
||||
[setGenerateDialog, setImageContextMenu],
|
||||
);
|
||||
|
||||
const updateIconDescription = useCallback(
|
||||
(index: number, value: string) => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
@@ -422,10 +496,14 @@ export function useImageCanvasGenerationWorkflow({
|
||||
iconDescriptionValues,
|
||||
isSpecMenuOpen,
|
||||
setIsSpecMenuOpen,
|
||||
isGenerationReferenceMenuOpen,
|
||||
setIsGenerationReferenceMenuOpen,
|
||||
isCharacterSpecMenuOpen,
|
||||
setIsCharacterSpecMenuOpen,
|
||||
isCharacterReferenceMenuOpen,
|
||||
setIsCharacterReferenceMenuOpen,
|
||||
isPickingGenerationReferenceFromCanvas,
|
||||
setIsPickingGenerationReferenceFromCanvas,
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
setIsPickingCharacterSpecFromCanvas,
|
||||
isPickingCharacterReferenceFromCanvas,
|
||||
@@ -434,6 +512,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
setIsIconSpecMenuOpen,
|
||||
isPickingIconSpecFromCanvas,
|
||||
setIsPickingIconSpecFromCanvas,
|
||||
isUiDesignSpecMenuOpen,
|
||||
setIsUiDesignSpecMenuOpen,
|
||||
isPickingUiDesignSpecFromCanvas,
|
||||
setIsPickingUiDesignSpecFromCanvas,
|
||||
openGenerateDialog,
|
||||
openSpecDialog,
|
||||
openCharacterAnimationPanel,
|
||||
@@ -442,8 +524,10 @@ export function useImageCanvasGenerationWorkflow({
|
||||
openEditDialog,
|
||||
openQuickEditPanel,
|
||||
pickCharacterSpecFromLayer,
|
||||
pickGenerationReferenceFromLayer,
|
||||
pickCharacterReferenceFromLayer,
|
||||
pickIconSpecFromLayer,
|
||||
pickUiDesignSpecFromLayer,
|
||||
submitIconSpritesheetGeneration,
|
||||
submitQuickEdit,
|
||||
submitImageGeneration,
|
||||
@@ -469,9 +553,13 @@ export function useImageCanvasGenerationWorkflow({
|
||||
isCharacterReferenceMenuOpen,
|
||||
isCharacterSpecMenuOpen,
|
||||
isIconSpecMenuOpen,
|
||||
isGenerationReferenceMenuOpen,
|
||||
isPickingCharacterReferenceFromCanvas,
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
isPickingGenerationReferenceFromCanvas,
|
||||
isPickingIconSpecFromCanvas,
|
||||
isPickingUiDesignSpecFromCanvas,
|
||||
isUiDesignSpecMenuOpen,
|
||||
isSpecMenuOpen,
|
||||
openCharacterAnimationPanel,
|
||||
openCharacterGenerationDialog,
|
||||
@@ -482,7 +570,9 @@ export function useImageCanvasGenerationWorkflow({
|
||||
openSpecDialog,
|
||||
pickCharacterReferenceFromLayer,
|
||||
pickCharacterSpecFromLayer,
|
||||
pickGenerationReferenceFromLayer,
|
||||
pickIconSpecFromLayer,
|
||||
pickUiDesignSpecFromLayer,
|
||||
quickEditModelOptions,
|
||||
quickEditPanel,
|
||||
quickEditSizeOptions,
|
||||
|
||||
@@ -193,6 +193,40 @@ export function useImageCanvasUploadWorkflow({
|
||||
[setGenerateDialog],
|
||||
);
|
||||
|
||||
const addGenerationReferenceFiles = useCallback(
|
||||
async (files: FileList | File[]) => {
|
||||
const imageFiles = Array.from(files).filter(isImageFile);
|
||||
if (!imageFiles.length) {
|
||||
window.alert('请选择图片文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const references = await Promise.all(
|
||||
imageFiles.map(async (file, index) => ({
|
||||
file,
|
||||
index,
|
||||
imageSrc: await readImageFileAsDataUrl(file),
|
||||
})),
|
||||
);
|
||||
setGenerateDialog((currentDialog) =>
|
||||
applyGenerationReferenceUpload({
|
||||
dialog: currentDialog,
|
||||
target: 'generation-reference',
|
||||
references: references.map(({ file, imageSrc, index }) =>
|
||||
createUploadedGenerationReference({
|
||||
idPrefix: 'upload-generation-reference',
|
||||
index,
|
||||
fileName: file.name,
|
||||
fallbackLabel: `参考图${index + 1}`,
|
||||
imageSrc,
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setGenerateDialog],
|
||||
);
|
||||
|
||||
const addIconSpecReferenceFiles = useCallback(
|
||||
async (files: FileList | File[]) => {
|
||||
const imageFile = Array.from(files).find(isImageFile);
|
||||
@@ -220,6 +254,33 @@ export function useImageCanvasUploadWorkflow({
|
||||
[setGenerateDialog],
|
||||
);
|
||||
|
||||
const addUiDesignSpecReferenceFiles = useCallback(
|
||||
async (files: FileList | File[]) => {
|
||||
const imageFile = Array.from(files).find(isImageFile);
|
||||
if (!imageFile) {
|
||||
window.alert('请选择图片文件');
|
||||
return;
|
||||
}
|
||||
|
||||
const imageSrc = await readImageFileAsDataUrl(imageFile);
|
||||
setGenerateDialog((currentDialog) =>
|
||||
applyGenerationReferenceUpload({
|
||||
dialog: currentDialog,
|
||||
target: 'ui-design-icon-spec',
|
||||
references: [
|
||||
createUploadedGenerationReference({
|
||||
idPrefix: 'upload-ui-design-icon-spec',
|
||||
fileName: imageFile.name,
|
||||
fallbackLabel: 'UI设计图标素材规范',
|
||||
imageSrc,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setGenerateDialog],
|
||||
);
|
||||
|
||||
const uploadAssetFile = useCallback(
|
||||
async (file: File, options: UploadAssetFileOptions) => {
|
||||
if (!isImageFile(file)) {
|
||||
@@ -435,12 +496,16 @@ export function useImageCanvasUploadWorkflow({
|
||||
if (files?.length) {
|
||||
if (currentUploadTarget === 'spec-reference') {
|
||||
void addSpecReferenceFiles(files);
|
||||
} else if (currentUploadTarget === 'generation-reference') {
|
||||
void addGenerationReferenceFiles(files);
|
||||
} else if (currentUploadTarget === 'character-spec') {
|
||||
void addCharacterSpecReferenceFiles(files);
|
||||
} else if (currentUploadTarget === 'character-reference') {
|
||||
void addCharacterReferenceFiles(files);
|
||||
} else if (currentUploadTarget === 'icon-spec') {
|
||||
void addIconSpecReferenceFiles(files);
|
||||
} else if (currentUploadTarget === 'ui-design-icon-spec') {
|
||||
void addUiDesignSpecReferenceFiles(files);
|
||||
} else {
|
||||
addUploadedFiles(files, { addToCanvas: activeTool === 'upload' });
|
||||
}
|
||||
@@ -452,8 +517,10 @@ export function useImageCanvasUploadWorkflow({
|
||||
activeTool,
|
||||
addCharacterReferenceFiles,
|
||||
addCharacterSpecReferenceFiles,
|
||||
addGenerationReferenceFiles,
|
||||
addSpecReferenceFiles,
|
||||
addIconSpecReferenceFiles,
|
||||
addUiDesignSpecReferenceFiles,
|
||||
addUploadedFiles,
|
||||
setUploadTarget,
|
||||
],
|
||||
@@ -469,6 +536,8 @@ export function useImageCanvasUploadWorkflow({
|
||||
addSpecReferenceFiles,
|
||||
addCharacterSpecReferenceFiles,
|
||||
addCharacterReferenceFiles,
|
||||
addGenerationReferenceFiles,
|
||||
addIconSpecReferenceFiles,
|
||||
addUiDesignSpecReferenceFiles,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user