ac180ecad9
新增画板底部生成音乐入口,支持游戏音效和游戏背景音乐生成面板。 接入编辑器音频生成前后端契约、VectorEngine 请求与 BFF 路由。 补齐音频结果图层、元数据、画布恢复和客户端提交流程。 统一生成类面板 Lovart 式参数浮层、参考素材入口与相关测试。 更新编辑器音乐生成设计文档和项目长期记忆。
246 lines
8.5 KiB
TypeScript
246 lines
8.5 KiB
TypeScript
import { ImageIcon } from 'lucide-react';
|
|
import {
|
|
type CSSProperties,
|
|
type Dispatch,
|
|
type ReactNode,
|
|
type RefObject,
|
|
type SetStateAction,
|
|
} from 'react';
|
|
|
|
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
|
|
import {
|
|
PlatformFloatingMenu,
|
|
PlatformFloatingMenuItem,
|
|
} from '../common/PlatformFloatingMenu';
|
|
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
|
|
import { PlatformTextField } from '../common/PlatformTextField';
|
|
import type {
|
|
GenerateDialogState,
|
|
SpecGenerationType,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
import { ImageCanvasGenerationImageOptionsView } from './ImageCanvasGenerationImageOptionsView';
|
|
import { ImageCanvasReferenceSlot } from './ImageCanvasReferenceSlot';
|
|
import {
|
|
DEFAULT_ICON_DESCRIPTIONS,
|
|
EDITOR_GENERATION_MUD_POINT_CONFIG,
|
|
ICON_DESCRIPTION_LIMIT,
|
|
} from './ImageCanvasGenerationModel';
|
|
import { useImageCanvasFloatingOptionDismiss } from './useImageCanvasFloatingOptionDismiss';
|
|
|
|
type ImageCanvasIconSpritesheetComposerViewProps = {
|
|
dialog: GenerateDialogState;
|
|
style: CSSProperties;
|
|
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
|
isIconSpecMenuOpen: boolean;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
renderEditorPortal: (node: ReactNode) => ReactNode;
|
|
buildPortalMenuStyle: (
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
) => CSSProperties;
|
|
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
|
onRequestUpload: (target: UploadTarget) => void;
|
|
onUpdateIconDescription: (index: number, value: string) => void;
|
|
onAddIconDescription: () => void;
|
|
onRememberImageModel: (model: string) => void;
|
|
onSubmit: (dialog: GenerateDialogState) => void;
|
|
};
|
|
|
|
export function ImageCanvasIconSpritesheetComposerView({
|
|
dialog,
|
|
style,
|
|
iconSpecButtonRef,
|
|
isIconSpecMenuOpen,
|
|
setGenerateDialog,
|
|
setIsIconSpecMenuOpen,
|
|
setIsPickingIconSpecFromCanvas,
|
|
renderEditorPortal,
|
|
buildPortalMenuStyle,
|
|
onOpenSpecDialog,
|
|
onRequestUpload,
|
|
onUpdateIconDescription,
|
|
onAddIconDescription,
|
|
onRememberImageModel,
|
|
onSubmit,
|
|
}: ImageCanvasIconSpritesheetComposerViewProps) {
|
|
const descriptions = dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS;
|
|
useImageCanvasFloatingOptionDismiss({
|
|
isOpen: isIconSpecMenuOpen,
|
|
boundaryRefs: [iconSpecButtonRef],
|
|
onDismiss: () => setIsIconSpecMenuOpen(false),
|
|
});
|
|
|
|
const pickIconSpecFromCanvas = () => {
|
|
setIsPickingIconSpecFromCanvas(true);
|
|
setIsIconSpecMenuOpen(false);
|
|
};
|
|
|
|
const openIconSpecDialog = () => {
|
|
setIsIconSpecMenuOpen(false);
|
|
onOpenSpecDialog('icon');
|
|
};
|
|
|
|
const requestIconSpecUpload = () => {
|
|
setIsIconSpecMenuOpen(false);
|
|
onRequestUpload('icon-spec');
|
|
};
|
|
|
|
return (
|
|
<form
|
|
className="image-canvas-editor__icon-composer"
|
|
style={style}
|
|
role="dialog"
|
|
aria-label="生成图标素材"
|
|
onPointerDown={(event) => event.stopPropagation()}
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
if (dialog.status !== 'generating') {
|
|
onSubmit(dialog);
|
|
}
|
|
}}
|
|
>
|
|
<div className="image-canvas-editor__reference-strip">
|
|
<ImageCanvasReferenceSlot
|
|
buttonRef={iconSpecButtonRef}
|
|
tone="icon"
|
|
className="image-canvas-editor__icon-spec-card"
|
|
icon={<ImageIcon className="h-4 w-4" aria-hidden="true" />}
|
|
imageSrc={dialog.iconSpecReference?.src}
|
|
label="图标规范"
|
|
ariaLabel={dialog.iconSpecReference?.label ?? '图标素材规范'}
|
|
title={dialog.iconSpecReference?.label ?? '图标素材规范'}
|
|
disabled={dialog.status === 'generating'}
|
|
isAdd={!dialog.iconSpecReference}
|
|
onClick={() => setIsIconSpecMenuOpen((open) => !open)}
|
|
onRemove={
|
|
dialog.iconSpecReference
|
|
? () =>
|
|
setGenerateDialog((currentDialog) =>
|
|
currentDialog?.mode === 'icon'
|
|
? {
|
|
...currentDialog,
|
|
status:
|
|
currentDialog.status === 'failed'
|
|
? 'idle'
|
|
: currentDialog.status,
|
|
errorMessage:
|
|
currentDialog.status === 'failed'
|
|
? undefined
|
|
: currentDialog.errorMessage,
|
|
iconSpecReference: null,
|
|
}
|
|
: currentDialog,
|
|
)
|
|
: undefined
|
|
}
|
|
removeLabel="删除图标素材规范"
|
|
/>
|
|
{isIconSpecMenuOpen
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__character-spec-menu image-canvas-editor__portal-menu"
|
|
label="图标素材规范来源"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(iconSpecButtonRef.current, 'above')}
|
|
>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={pickIconSpecFromCanvas}
|
|
>
|
|
从画布中选择
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={openIconSpecDialog}
|
|
>
|
|
新建图标素材规范
|
|
</PlatformFloatingMenuItem>
|
|
<PlatformFloatingMenuItem
|
|
className="image-canvas-editor__context-menu-item"
|
|
onClick={requestIconSpecUpload}
|
|
>
|
|
上传图片
|
|
</PlatformFloatingMenuItem>
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
</div>
|
|
<div className="image-canvas-editor__field-block">
|
|
<PlatformFieldLabel
|
|
variant="field"
|
|
className="image-canvas-editor__field-title"
|
|
>
|
|
素材描述
|
|
</PlatformFieldLabel>
|
|
<div className="image-canvas-editor__icon-description-list">
|
|
{descriptions.map((description, index) => (
|
|
<label
|
|
key={index}
|
|
className="image-canvas-editor__icon-description-card"
|
|
>
|
|
<span className="image-canvas-editor__icon-description-index">
|
|
{String(index + 1).padStart(2, '0')}
|
|
</span>
|
|
<span className="image-canvas-editor__icon-description-title">
|
|
素材描述 {index + 1}
|
|
</span>
|
|
<PlatformTextField
|
|
aria-label={`素材描述${index + 1}`}
|
|
value={description}
|
|
disabled={dialog.status === 'generating'}
|
|
size="sm"
|
|
density="compact"
|
|
className="image-canvas-editor__icon-description-input"
|
|
onChange={(event) =>
|
|
onUpdateIconDescription(index, event.target.value)
|
|
}
|
|
/>
|
|
</label>
|
|
))}
|
|
</div>
|
|
</div>
|
|
{dialog.status === 'failed' ? (
|
|
<PlatformStatusMessage
|
|
tone="error"
|
|
surface="platform"
|
|
size="xs"
|
|
className="image-canvas-editor__generate-status"
|
|
role="alert"
|
|
>
|
|
{dialog.errorMessage}
|
|
</PlatformStatusMessage>
|
|
) : null}
|
|
<div className="image-canvas-editor__icon-footer">
|
|
<button
|
|
type="button"
|
|
className="image-canvas-editor__character-reference-add"
|
|
disabled={
|
|
dialog.status === 'generating' ||
|
|
descriptions.length >= ICON_DESCRIPTION_LIMIT
|
|
}
|
|
onClick={onAddIconDescription}
|
|
>
|
|
添加素材描述
|
|
</button>
|
|
</div>
|
|
<div className="image-canvas-editor__generation-composer-footer">
|
|
<ImageCanvasGenerationImageOptionsView
|
|
dialog={dialog}
|
|
setGenerateDialog={setGenerateDialog}
|
|
includeDimensions
|
|
onRememberImageModel={onRememberImageModel}
|
|
optionLabelPrefix="生成图片"
|
|
cost={EDITOR_GENERATION_MUD_POINT_CONFIG.icon}
|
|
submitLabel="生成"
|
|
submitAriaLabel="生成"
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
/>
|
|
</div>
|
|
</form>
|
|
);
|
|
}
|