d8b935317d
抽出素材栏、生成器、舞台工具栏和画布世界视图 补充各拆分视图的聚焦测试 更新 TRACKING.md 记录第三十四阶段验证
313 lines
11 KiB
TypeScript
313 lines
11 KiB
TypeScript
import {
|
|
type CSSProperties,
|
|
type Dispatch,
|
|
type ReactNode,
|
|
type RefObject,
|
|
type SetStateAction,
|
|
} from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
|
|
import {
|
|
PlatformFloatingMenu,
|
|
PlatformFloatingMenuItem,
|
|
} from '../common/PlatformFloatingMenu';
|
|
import { ImageCanvasBasicGenerationComposerView } from './ImageCanvasBasicGenerationComposerView';
|
|
import { ImageCanvasCharacterAnimationPanelView } from './ImageCanvasCharacterAnimationPanelView';
|
|
import { ImageCanvasCharacterGenerationComposerView } from './ImageCanvasCharacterGenerationComposerView';
|
|
import { ImageCanvasEditGenerationModalView } from './ImageCanvasEditGenerationModalView';
|
|
import { ImageCanvasIconSpritesheetComposerView } from './ImageCanvasIconSpritesheetComposerView';
|
|
import { ImageCanvasQuickEditPanelView } from './ImageCanvasQuickEditPanelView';
|
|
import { ImageCanvasSpecGenerationPanelView } from './ImageCanvasSpecGenerationPanelView';
|
|
import { SPEC_TYPE_LABEL } from './ImageCanvasGenerationModel';
|
|
import type {
|
|
CharacterAnimationPanelState,
|
|
CanvasLayer,
|
|
GenerateDialogState,
|
|
QuickEditPanelState,
|
|
SpecFormValues,
|
|
SpecGenerationType,
|
|
UploadTarget,
|
|
} from './ImageCanvasEditorTypes';
|
|
|
|
type ImageCanvasGenerationComposerViewProps = {
|
|
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
|
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
|
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
|
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
|
isSpecMenuOpen: boolean;
|
|
isCharacterSpecMenuOpen: boolean;
|
|
isCharacterReferenceMenuOpen: boolean;
|
|
isIconSpecMenuOpen: boolean;
|
|
isPickingCharacterSpecFromCanvas: boolean;
|
|
isPickingCharacterReferenceFromCanvas: boolean;
|
|
isPickingIconSpecFromCanvas: boolean;
|
|
generateDialog: GenerateDialogState | null;
|
|
generationComposerStyle: CSSProperties | null;
|
|
iconComposerStyle: CSSProperties | null;
|
|
quickEditPanel: QuickEditPanelState | null;
|
|
quickEditSourceLayer: CanvasLayer | null;
|
|
quickEditPanelStyle: CSSProperties | null;
|
|
quickEditSizeOptions: string[];
|
|
quickEditModelOptions: Array<{ label: string; value: string }>;
|
|
characterAnimationPanel: CharacterAnimationPanelState | null;
|
|
characterAnimationSourceLayer: CanvasLayer | null;
|
|
characterAnimationPanelStyle: CSSProperties | null;
|
|
characterAnimationPrice: number;
|
|
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
|
setQuickEditPanel: Dispatch<SetStateAction<QuickEditPanelState | null>>;
|
|
setCharacterAnimationPanel: Dispatch<
|
|
SetStateAction<CharacterAnimationPanelState | null>
|
|
>;
|
|
setIsCharacterSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsCharacterReferenceMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsIconSpecMenuOpen: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingCharacterSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingCharacterReferenceFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
setIsPickingIconSpecFromCanvas: Dispatch<SetStateAction<boolean>>;
|
|
onOpenSpecDialog: (specType: SpecGenerationType) => void;
|
|
onRequestUpload: (target: UploadTarget) => void;
|
|
onSubmitImageGeneration: (dialog: GenerateDialogState) => void;
|
|
onSubmitIconSpritesheetGeneration: (dialog: GenerateDialogState) => void;
|
|
onSubmitQuickEdit: () => void;
|
|
onSubmitCharacterAnimation: () => void;
|
|
onCloseGenerateComposer: () => void;
|
|
onUpdateSpecFormValue: (key: keyof SpecFormValues, value: string) => void;
|
|
onUpdateIconDescription: (index: number, value: string) => void;
|
|
onAddIconDescription: () => void;
|
|
onUpdateCharacterAnimationDuration: (frameCountValue: string) => void;
|
|
onRememberImageModel: (model: string) => void;
|
|
};
|
|
|
|
function buildPortalMenuStyle(
|
|
anchor: HTMLElement | null,
|
|
placement: 'above' | 'below',
|
|
): CSSProperties {
|
|
const rect = anchor?.getBoundingClientRect();
|
|
if (!rect) {
|
|
return {
|
|
position: 'fixed',
|
|
left: 0,
|
|
top: 0,
|
|
right: 'auto',
|
|
bottom: 'auto',
|
|
zIndex: 70,
|
|
};
|
|
}
|
|
|
|
return {
|
|
position: 'fixed',
|
|
left: Math.round(rect.left),
|
|
top:
|
|
placement === 'above'
|
|
? Math.round(rect.top)
|
|
: Math.round(rect.bottom + 8),
|
|
right: 'auto',
|
|
bottom: 'auto',
|
|
zIndex: 70,
|
|
transform:
|
|
placement === 'above' ? 'translateY(calc(-100% - 0.45rem))' : undefined,
|
|
};
|
|
}
|
|
|
|
function renderEditorPortal(node: ReactNode) {
|
|
if (typeof document === 'undefined') {
|
|
return node;
|
|
}
|
|
return createPortal(node, document.body);
|
|
}
|
|
|
|
export function ImageCanvasGenerationComposerView({
|
|
specToolWrapRef,
|
|
characterSpecButtonRef,
|
|
characterReferenceButtonRef,
|
|
iconSpecButtonRef,
|
|
isSpecMenuOpen,
|
|
isCharacterSpecMenuOpen,
|
|
isCharacterReferenceMenuOpen,
|
|
isIconSpecMenuOpen,
|
|
isPickingCharacterSpecFromCanvas,
|
|
isPickingCharacterReferenceFromCanvas,
|
|
isPickingIconSpecFromCanvas,
|
|
generateDialog,
|
|
generationComposerStyle,
|
|
iconComposerStyle,
|
|
quickEditPanel,
|
|
quickEditSourceLayer,
|
|
quickEditPanelStyle,
|
|
quickEditSizeOptions,
|
|
quickEditModelOptions,
|
|
characterAnimationPanel,
|
|
characterAnimationSourceLayer,
|
|
characterAnimationPanelStyle,
|
|
characterAnimationPrice,
|
|
setGenerateDialog,
|
|
setQuickEditPanel,
|
|
setCharacterAnimationPanel,
|
|
setIsCharacterSpecMenuOpen,
|
|
setIsCharacterReferenceMenuOpen,
|
|
setIsIconSpecMenuOpen,
|
|
setIsPickingCharacterSpecFromCanvas,
|
|
setIsPickingCharacterReferenceFromCanvas,
|
|
setIsPickingIconSpecFromCanvas,
|
|
onOpenSpecDialog,
|
|
onRequestUpload,
|
|
onSubmitImageGeneration,
|
|
onSubmitIconSpritesheetGeneration,
|
|
onSubmitQuickEdit,
|
|
onSubmitCharacterAnimation,
|
|
onCloseGenerateComposer,
|
|
onUpdateSpecFormValue,
|
|
onUpdateIconDescription,
|
|
onAddIconDescription,
|
|
onUpdateCharacterAnimationDuration,
|
|
onRememberImageModel,
|
|
}: ImageCanvasGenerationComposerViewProps) {
|
|
return (
|
|
<>
|
|
{isSpecMenuOpen
|
|
? renderEditorPortal(
|
|
<PlatformFloatingMenu
|
|
className="image-canvas-editor__spec-menu image-canvas-editor__portal-menu"
|
|
label="生成规范类型"
|
|
placement="top-start"
|
|
style={buildPortalMenuStyle(specToolWrapRef.current, 'above')}
|
|
>
|
|
{(['character', 'ui', 'custom'] as const).map((specType) => (
|
|
<PlatformFloatingMenuItem
|
|
key={specType}
|
|
className="image-canvas-editor__spec-menu-item"
|
|
onClick={() => onOpenSpecDialog(specType)}
|
|
>
|
|
{SPEC_TYPE_LABEL[specType]}
|
|
</PlatformFloatingMenuItem>
|
|
))}
|
|
</PlatformFloatingMenu>,
|
|
)
|
|
: null}
|
|
|
|
{generateDialog?.mode === 'generate' &&
|
|
generateDialog.composerOpen !== false &&
|
|
generationComposerStyle ? (
|
|
<ImageCanvasBasicGenerationComposerView
|
|
dialog={generateDialog}
|
|
style={generationComposerStyle}
|
|
setGenerateDialog={setGenerateDialog}
|
|
onRequestUpload={onRequestUpload}
|
|
onSubmit={onSubmitImageGeneration}
|
|
onClose={onCloseGenerateComposer}
|
|
/>
|
|
) : null}
|
|
|
|
{generateDialog?.mode === 'spec' &&
|
|
generateDialog.composerOpen !== false &&
|
|
generationComposerStyle ? (
|
|
<ImageCanvasSpecGenerationPanelView
|
|
dialog={generateDialog}
|
|
style={generationComposerStyle}
|
|
onUpdateSpecFormValue={onUpdateSpecFormValue}
|
|
onRequestUpload={onRequestUpload}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{generateDialog?.mode === 'character' && generationComposerStyle ? (
|
|
<ImageCanvasCharacterGenerationComposerView
|
|
dialog={generateDialog}
|
|
style={generationComposerStyle}
|
|
characterSpecButtonRef={characterSpecButtonRef}
|
|
characterReferenceButtonRef={characterReferenceButtonRef}
|
|
isCharacterSpecMenuOpen={isCharacterSpecMenuOpen}
|
|
isCharacterReferenceMenuOpen={isCharacterReferenceMenuOpen}
|
|
setGenerateDialog={setGenerateDialog}
|
|
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
|
|
setIsCharacterReferenceMenuOpen={setIsCharacterReferenceMenuOpen}
|
|
setIsPickingCharacterSpecFromCanvas={
|
|
setIsPickingCharacterSpecFromCanvas
|
|
}
|
|
setIsPickingCharacterReferenceFromCanvas={
|
|
setIsPickingCharacterReferenceFromCanvas
|
|
}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
onOpenSpecDialog={onOpenSpecDialog}
|
|
onRequestUpload={onRequestUpload}
|
|
onRememberImageModel={onRememberImageModel}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{generateDialog?.mode === 'icon' &&
|
|
generateDialog.composerOpen !== false &&
|
|
iconComposerStyle ? (
|
|
<ImageCanvasIconSpritesheetComposerView
|
|
dialog={generateDialog}
|
|
style={iconComposerStyle}
|
|
iconSpecButtonRef={iconSpecButtonRef}
|
|
isIconSpecMenuOpen={isIconSpecMenuOpen}
|
|
setGenerateDialog={setGenerateDialog}
|
|
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
|
|
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
|
|
renderEditorPortal={renderEditorPortal}
|
|
buildPortalMenuStyle={buildPortalMenuStyle}
|
|
onOpenSpecDialog={onOpenSpecDialog}
|
|
onRequestUpload={onRequestUpload}
|
|
onUpdateIconDescription={onUpdateIconDescription}
|
|
onAddIconDescription={onAddIconDescription}
|
|
onRememberImageModel={onRememberImageModel}
|
|
onSubmit={onSubmitIconSpritesheetGeneration}
|
|
/>
|
|
) : null}
|
|
|
|
{isPickingCharacterSpecFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图片作为角色形象规范,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
{isPickingCharacterReferenceFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图片作为常规参考图,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
{isPickingIconSpecFromCanvas ? (
|
|
<div className="image-canvas-editor__canvas-pick-hint">
|
|
请选择画布中的图标素材规范,按 Esc 退出
|
|
</div>
|
|
) : null}
|
|
|
|
{quickEditPanel &&
|
|
quickEditPanel.status !== 'generating' &&
|
|
quickEditSourceLayer &&
|
|
quickEditPanelStyle ? (
|
|
<ImageCanvasQuickEditPanelView
|
|
panel={quickEditPanel}
|
|
sourceLayer={quickEditSourceLayer}
|
|
style={quickEditPanelStyle}
|
|
sizeOptions={quickEditSizeOptions}
|
|
modelOptions={quickEditModelOptions}
|
|
setQuickEditPanel={setQuickEditPanel}
|
|
onSubmit={onSubmitQuickEdit}
|
|
/>
|
|
) : null}
|
|
|
|
{characterAnimationPanel &&
|
|
characterAnimationSourceLayer &&
|
|
characterAnimationPanelStyle ? (
|
|
<ImageCanvasCharacterAnimationPanelView
|
|
panel={characterAnimationPanel}
|
|
style={characterAnimationPanelStyle}
|
|
price={characterAnimationPrice}
|
|
setCharacterAnimationPanel={setCharacterAnimationPanel}
|
|
onUpdateDuration={onUpdateCharacterAnimationDuration}
|
|
onSubmit={onSubmitCharacterAnimation}
|
|
/>
|
|
) : null}
|
|
|
|
<ImageCanvasEditGenerationModalView
|
|
dialog={generateDialog}
|
|
setGenerateDialog={setGenerateDialog}
|
|
onSubmit={onSubmitImageGeneration}
|
|
/>
|
|
</>
|
|
);
|
|
}
|