diff --git a/TRACKING.md b/TRACKING.md index b3698a2b..173f757f 100644 --- a/TRACKING.md +++ b/TRACKING.md @@ -116,3 +116,5 @@ - 2026-06-17 侧栏拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`;`画布背景色` 打开 `画布背景设置` dialog,包含预设、自定义颜色、HEX 和恢复默认;使用临时开发账号登录后上传图片成功进入 `项目素材`,点击素材可添加到画布,切换 `图层` 侧栏后能看到同一图片图层,`AI画布工具栏` 保持可见。 - 2026-06-17 前端拆分第三阶段:新增 `ImageCanvasStageView`,把画布工作区视觉树、图层渲染、生成占位框、右键菜单、左下 dock、小地图和底部 AI 工具栏从主视图抽出;拖拽 / 缩放、历史、上传、登录、生成提交、素材持久化和右键命令仍保留在主视图,避免拆散状态机。 - 2026-06-17 舞台拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 未登录刷新弹出 `账号入口`;关闭登录后点击 `画布背景色` 打开完整 `画布背景设置` dialog,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`;使用临时开发账号密码登录后上传 `smoke.png` 成功进入 `项目素材`,点击素材添加到画布,切换 `图层` 后显示同一图层,图片浮动工具栏、小地图和 `AI画布工具栏` 保持可见。 +- 2026-06-17 前端拆分第四阶段:新增 `ImageCanvasGenerationComposerView`,把生成图片、生成规范、生成角色形象、生成图标素材、快速编辑、角色动画和修改图片弹窗从主视图抽出;生成提交、上传 input、引用选择、占位框拖拽、结果回写、历史和画布状态机仍保留在主视图。验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorPrimitives.test.tsx`、`npm run typecheck`。 +- 2026-06-17 生成面板拆分浏览器回归:`http://127.0.0.1:10003/editor/canvas` 清空浏览器数据后未登录刷新弹出 `账号入口`;关闭登录后 `画布背景色` 打开 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)`;点击 `生成工具` 后画布显示 `Image Generator` 占位框和 `生成图片` 跟随对话框,`AI画布工具栏` 保持可见;使用临时开发账号密码登录后上传素材成功,点击素材可添加到画布,切换 `图层` 面板可看到对应图层。 diff --git a/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md b/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md index b246398d..478a7274 100644 --- a/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md +++ b/docs/technical/【前端架构】图片画布编辑器前端拆分计划-2026-06-17.md @@ -50,9 +50,17 @@ 第三阶段以后,主视图仍是画布编排入口。继续拆分前应优先选择能形成稳定边界的深模块,避免把上传链路、DataTransfer、画布坐标和历史快照拆成互相回调的小碎片。 +## 第四阶段模块 + +- `ImageCanvasGenerationComposerView.tsx` + - 承载生成图片、生成规范、生成角色形象、生成图标素材、快速编辑图片、角色动画和修改图片弹窗的视觉表单。 + - 保留生成对象状态机、提交 API、上传文件 input、引用选择、生成结果回写、图层历史和坐标锚定在主视图内,避免把 Lovart 式生成对象拆成不可追踪的远程状态。 + - 该组件可以管理局部字段输入和菜单展示,但所有会影响画布事实的动作都通过主视图回调落回原有状态机。 + ## 后续阶段 -- `ImageCanvasGenerationDock`:底部 AI 工具栏和生成面板族,等生成对象状态机进一步收口后再拆。 +- 生成状态机模型:等生成对象归档、占位框拖拽、生成完成回写、失败恢复和 undo / redo 规则进一步稳定后,再从主视图抽出深层状态模型。 +- 画布命令模型:右键菜单、图层层级、分组、锁定和隐藏命令可在保持历史快照一致后继续收口。 ## 验证计划 diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index 1ce17e8a..525d7aae 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -1,11 +1,7 @@ import { Check, - ChevronDown, ChevronLeft, - ClipboardList, Download, - ImageIcon, - ImagePlus, Pencil, X, } from 'lucide-react'; @@ -15,7 +11,6 @@ import { type DragEvent as ReactDragEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, - type ReactNode, useCallback, useEffect, useMemo, @@ -23,7 +18,6 @@ import { useState, type WheelEvent as ReactWheelEvent, } from 'react'; -import { createPortal } from 'react-dom'; import { ApiClientError } from '../../services/apiClient'; import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference'; @@ -48,22 +42,12 @@ import { updateEditorAsset, updateEditorAssetFolder, } from '../../services/image-editor/editorProjectClient'; -import { PlatformActionButton } from '../common/PlatformActionButton'; -import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; -import { - PlatformFloatingMenu, - PlatformFloatingMenuItem, -} from '../common/PlatformFloatingMenu'; -import { PlatformIconButton } from '../common/PlatformIconButton'; -import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; -import { - PlatformSelectField, - PlatformTextField, -} from '../common/PlatformTextField'; +import { PlatformTextField } from '../common/PlatformTextField'; import { UnifiedModal } from '../common/UnifiedModal'; import { useAuthUi } from '../auth/AuthUiContext'; import { EditorIconButton } from './ImageCanvasEditorPrimitives'; +import { ImageCanvasGenerationComposerView } from './ImageCanvasGenerationComposerView'; import { ImageCanvasSidebarView } from './ImageCanvasSidebarView'; import { ImageCanvasStageView } from './ImageCanvasStageView'; import { @@ -106,13 +90,10 @@ import { sanitizeExportFilePart, } from './ImageCanvasExportModel'; import { - CHARACTER_ANIMATION_ACTION_PROMPTS, CHARACTER_ANIMATION_DURATION_OPTIONS, CHARACTER_ANIMATION_MODEL, - CHARACTER_ANIMATION_RATIO_OPTIONS, CHARACTER_FRAME_DISPLAY_SIZE, CHARACTER_FRAME_ORIGINAL_SIZE, - CHARACTER_SPEC_VIEW_OPTIONS, DEFAULT_ICON_DESCRIPTIONS, DEFAULT_IMAGE_MODEL, DEFAULT_SPEC_FORM_VALUES, @@ -124,7 +105,6 @@ import { ICON_FRAME_ORIGINAL_SIZE, SPEC_FRAME_DISPLAY_SIZE, SPEC_FRAME_ORIGINAL_SIZE, - SPEC_GENERATION_COST, SPEC_GENERATION_SIZE, SPEC_TYPE_LABEL, buildCharacterGenerationInputs, @@ -173,48 +153,6 @@ import type { UploadTarget, } from './ImageCanvasEditorTypes'; -function triggerPlaceholderAction(label: string) { - window.alert(`${label}功能建设中`); -} - -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); -} - function isImageFile(file: File) { return file.type.startsWith('image/'); } @@ -4446,1128 +4384,62 @@ export function ImageCanvasEditorView() { onMinimapPointerDown={handleMinimapPointerDown} onSwitchTool={switchTool} > - - {isSpecMenuOpen - ? renderEditorPortal( - - {(['character', 'ui', 'custom'] as const).map((specType) => ( - openSpecDialog(specType)} - > - {SPEC_TYPE_LABEL[specType]} - - ))} - , - ) - : null} - - {generateDialog?.mode === 'generate' && - generateDialog.composerOpen !== false && - generationComposerStyle ? ( -
event.stopPropagation()} - onSubmit={(event) => { - event.preventDefault(); - if (generateDialog.status !== 'generating') { - void submitImageGeneration(generateDialog); - } - }} - > - { - setUploadTarget('asset'); - uploadInputRef.current?.click(); - }} - icon={} - > - 参考图 - - - setGenerateDialog((currentDialog) => - currentDialog - ? { - ...currentDialog, - prompt: event.target.value, - status: - currentDialog.status === 'failed' - ? 'idle' - : currentDialog.status, - errorMessage: - currentDialog.status === 'failed' - ? undefined - : currentDialog.errorMessage, - } - : currentDialog, - ) - } - /> -
- triggerPlaceholderAction('生成参数')} - trailingIcon={} - > - 中 · 1:1(2k) · 1张 - - triggerPlaceholderAction('模型选择')} - trailingIcon={} - > - GPT Im... - - - {generateDialog.status === 'generating' ? '生成中' : '12'} - -
- {generateDialog.status === 'generating' ? ( - - 生成中 - - ) : null} - {generateDialog.status === 'failed' ? ( - - {generateDialog.errorMessage} - - ) : null} - { - setGenerateDialog((currentDialog) => - currentDialog?.mode === 'generate' - ? { - ...currentDialog, - composerOpen: false, - } - : currentDialog, - ); - setActiveTool('select'); - }} - /> - - ) : null} - - {generateDialog?.mode === 'spec' && - generateDialog.composerOpen !== false && - generationComposerStyle ? ( -
event.stopPropagation()} - onSubmit={(event) => { - event.preventDefault(); - if (generateDialog.status !== 'generating') { - void submitImageGeneration(generateDialog); - } - }} - > -
- {generateDialog.specType === 'custom' ? ( - - ) : ( - <> - - - {generateDialog.specType === 'character' ? ( - <> - - - - ) : null} - - )} -
- {generateDialog.status === 'failed' ? ( - - {generateDialog.errorMessage} - - ) : null} -
- - {generateDialog.status === 'generating' - ? '生成中' - : `消耗${SPEC_GENERATION_COST}泥点 · 生成`} - -
-
- ) : null} - - {generateDialog?.mode === 'character' && generationComposerStyle ? ( -
event.stopPropagation()} - onSubmit={(event) => { - event.preventDefault(); - if (generateDialog.status !== 'generating') { - void submitImageGeneration(generateDialog); - } - }} - > -
-
- - 角色形象规范 - - - - -
- {isCharacterSpecMenuOpen - ? renderEditorPortal( - - { - setIsPickingCharacterSpecFromCanvas(true); - setIsCharacterSpecMenuOpen(false); - }} - > - 从画布中选择 - - { - setIsCharacterSpecMenuOpen(false); - openSpecDialog('character'); - }} - > - 新建角色形象规范 - - { - setUploadTarget('character-spec'); - setIsCharacterSpecMenuOpen(false); - uploadInputRef.current?.click(); - }} - > - 上传图片 - - , - ) - : null} -
- - 常规参考图 - -
- {(generateDialog.characterReferences ?? []).map( - (reference, index) => ( - - {reference.label} - - {index + 1} - - - ), - )} - -
-
-
- - {generateDialog.status === 'failed' ? ( - - {generateDialog.errorMessage} - - ) : null} -
-
- - 画面比例 - - triggerPlaceholderAction('角色比例')} - > - 1:1 - -
-
- - 模型 - - triggerPlaceholderAction('角色模型')} - > - GPT Image - -
- - {generateDialog.status === 'generating' ? '生成中' : '生成'} - -
-
- ) : null} - - {generateDialog?.mode === 'icon' && - generateDialog.composerOpen !== false && - iconComposerStyle ? ( -
event.stopPropagation()} - onSubmit={(event) => { - event.preventDefault(); - if (generateDialog.status !== 'generating') { - void submitIconSpritesheetGeneration(generateDialog); - } - }} - > -
- - 图标素材规范 - -
- - - - {isIconSpecMenuOpen - ? renderEditorPortal( - - { - setIsPickingIconSpecFromCanvas(true); - setIsIconSpecMenuOpen(false); - }} - > - 从画布中选择 - - { - setIsIconSpecMenuOpen(false); - openSpecDialog('icon'); - }} - > - 新建图标素材规范 - - { - setUploadTarget('icon-spec'); - setIsIconSpecMenuOpen(false); - uploadInputRef.current?.click(); - }} - > - 上传图片 - - , - ) - : null} -
- - - -
-
-
-
- - 素材描述 - -
- {iconDescriptionValues.map((description, index) => ( - - ))} -
-
- {generateDialog.status === 'failed' ? ( - - {generateDialog.errorMessage} - - ) : null} -
- -
- - 模型 - - triggerPlaceholderAction('图标模型')} - > - nanobanana2 - -
- - {generateDialog.status === 'generating' ? '生成中' : '生成'} - -
-
- ) : null} - - {isPickingCharacterSpecFromCanvas ? ( -
- 请选择画布中的图片作为角色形象规范,按 Esc 退出 -
- ) : null} - {isPickingIconSpecFromCanvas ? ( -
- 请选择画布中的图标素材规范,按 Esc 退出 -
- ) : null} - - {quickEditPanel && - quickEditPanel.status !== 'generating' && - quickEditSourceLayer && - quickEditPanelStyle ? ( -
event.stopPropagation()} - onSubmit={(event) => { - event.preventDefault(); - void submitQuickEdit(); - }} - > -
-
- {`${quickEditSourceLayer.title}参考图`} - {quickEditSourceLayer.title} -
- setQuickEditPanel(null)} - /> -
- - setQuickEditPanel((currentPanel) => - currentPanel - ? { - ...currentPanel, - prompt: event.target.value, - status: - currentPanel.status === 'failed' - ? 'idle' - : currentPanel.status, - errorMessage: - currentPanel.status === 'failed' - ? undefined - : currentPanel.errorMessage, - } - : currentPanel, - ) - } - /> -
- - setQuickEditPanel((currentPanel) => - currentPanel - ? { ...currentPanel, size: event.target.value } - : currentPanel, - ) - } - > - {quickEditSizeOptions.map((size) => ( - - ))} - - - setQuickEditPanel((currentPanel) => - currentPanel - ? { ...currentPanel, model: event.target.value } - : currentPanel, - ) - } - > - {quickEditModelOptions.map((option) => ( - - ))} - -
- {quickEditPanel.status === 'failed' ? ( - - {quickEditPanel.errorMessage} - - ) : null} - - 生成 - - - ) : null} - - - - {characterAnimationPanel && - characterAnimationSourceLayer && - characterAnimationPanelStyle ? ( -
event.stopPropagation()} - onSubmit={(event) => { - event.preventDefault(); - if (characterAnimationPanel.status !== 'generating') { - void submitCharacterAnimation(); - } - }} - > -
- 角色动画 - setCharacterAnimationPanel(null)} - /> -
- - setCharacterAnimationPanel((currentPanel) => - currentPanel - ? { - ...currentPanel, - promptText: event.target.value.slice(0, 4000), - status: - currentPanel.status === 'failed' - ? 'idle' - : currentPanel.status, - errorMessage: - currentPanel.status === 'failed' - ? undefined - : currentPanel.errorMessage, - } - : currentPanel, - ) - } - /> -
- {CHARACTER_ANIMATION_ACTION_PROMPTS.map((preset) => ( - - ))} -
-
- - setCharacterAnimationPanel((currentPanel) => - currentPanel - ? { - ...currentPanel, - resolution: - event.target.value === '720p' ? '720p' : '480p', - status: - currentPanel.status === 'failed' - ? 'idle' - : currentPanel.status, - errorMessage: - currentPanel.status === 'failed' - ? undefined - : currentPanel.errorMessage, - } - : currentPanel, - ) - } - > - - - - - setCharacterAnimationPanel((currentPanel) => - currentPanel - ? { - ...currentPanel, - ratio: - CHARACTER_ANIMATION_RATIO_OPTIONS.find( - (item) => item.value === event.target.value, - )?.value ?? 'same', - status: - currentPanel.status === 'failed' - ? 'idle' - : currentPanel.status, - errorMessage: - currentPanel.status === 'failed' - ? undefined - : currentPanel.errorMessage, - } - : currentPanel, - ) - } - > - {CHARACTER_ANIMATION_RATIO_OPTIONS.map((option) => ( - - ))} - - - updateCharacterAnimationDuration(event.target.value) - } - > - {CHARACTER_ANIMATION_DURATION_OPTIONS.map((option) => ( - - ))} - -
-
- - {characterAnimationPanel.promptText.trim() - ? characterAnimationPanel.promptText.trim() - : '动画描述'} - - {characterAnimationPrice}泥点 -
- {characterAnimationPanel.status === 'completed' && - characterAnimationPanel.result ? ( - - 已生成 {characterAnimationPanel.result.frameCount} 帧 - - ) : null} - {characterAnimationPanel.status === 'failed' ? ( - - {characterAnimationPanel.errorMessage} - - ) : null} - - {characterAnimationPanel.status === 'generating' - ? '生成中' - : '生成'} - - - ) : null} + : currentDialog, + ); + setActiveTool('select'); + }} + onUpdateSpecFormValue={updateSpecFormValue} + onUpdateIconDescription={updateIconDescription} + onAddIconDescription={addIconDescription} + onUpdateCharacterAnimationDuration={updateCharacterAnimationDuration} + /> + @@ -5642,97 +4514,6 @@ export function ImageCanvasEditorView() { ) : null} - - setGenerateDialog(null)} - panelClassName="image-canvas-editor__generate-dialog" - bodyClassName="image-canvas-editor__generate-dialog-body" - > - {generateDialog?.mode === 'edit' ? ( -
{ - event.preventDefault(); - if (generateDialog.status !== 'generating') { - void submitImageGeneration(generateDialog); - } - }} - > -
- - setGenerateDialog((currentDialog) => - currentDialog - ? { - ...currentDialog, - prompt: event.target.value, - } - : currentDialog, - ) - } - /> - {generateDialog.status === 'generating' ? ( - - {generateDialog.mode === 'edit' ? '修改中' : '生成中'} - - ) : null} - {generateDialog.status === 'failed' ? ( - - {generateDialog.errorMessage} - - ) : null} - - {generateDialog.status === 'generating' - ? generateDialog.mode === 'edit' - ? '修改中' - : '生成中' - : generateDialog.mode === 'edit' - ? '修改' - : '生成'} - -
-
- ) : null} -
); } diff --git a/src/components/image-editor/ImageCanvasGenerationComposerView.tsx b/src/components/image-editor/ImageCanvasGenerationComposerView.tsx new file mode 100644 index 00000000..444c363f --- /dev/null +++ b/src/components/image-editor/ImageCanvasGenerationComposerView.tsx @@ -0,0 +1,1318 @@ +import { + ChevronDown, + ClipboardList, + ImageIcon, + ImagePlus, + X, +} from 'lucide-react'; +import { + type CSSProperties, + type Dispatch, + type ReactNode, + type RefObject, + type SetStateAction, +} from 'react'; +import { createPortal } from 'react-dom'; + +import { PlatformActionButton } from '../common/PlatformActionButton'; +import { PlatformFieldLabel } from '../common/PlatformFieldLabel'; +import { + PlatformFloatingMenu, + PlatformFloatingMenuItem, +} from '../common/PlatformFloatingMenu'; +import { PlatformIconButton } from '../common/PlatformIconButton'; +import { PlatformInlineOptionButton } from '../common/PlatformInlineOptionButton'; +import { PlatformStatusMessage } from '../common/PlatformStatusMessage'; +import { + PlatformSelectField, + PlatformTextField, +} from '../common/PlatformTextField'; +import { UnifiedModal } from '../common/UnifiedModal'; +import { EditorIconButton } from './ImageCanvasEditorPrimitives'; +import { + CHARACTER_ANIMATION_ACTION_PROMPTS, + CHARACTER_ANIMATION_DURATION_OPTIONS, + CHARACTER_ANIMATION_RATIO_OPTIONS, + CHARACTER_SPEC_VIEW_OPTIONS, + DEFAULT_ICON_DESCRIPTIONS, + ICON_DESCRIPTION_LIMIT, + SPEC_GENERATION_COST, + SPEC_TYPE_LABEL, +} from './ImageCanvasGenerationModel'; +import type { + CharacterAnimationPanelState, + CanvasLayer, + GenerateDialogState, + QuickEditPanelState, + SpecFormValues, + SpecGenerationType, + UploadTarget, +} from './ImageCanvasEditorTypes'; + +type ImageCanvasGenerationComposerViewProps = { + specToolWrapRef: RefObject; + characterSpecButtonRef: RefObject; + iconSpecButtonRef: RefObject; + isSpecMenuOpen: boolean; + isCharacterSpecMenuOpen: boolean; + isIconSpecMenuOpen: boolean; + isPickingCharacterSpecFromCanvas: 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>; + setQuickEditPanel: Dispatch>; + setCharacterAnimationPanel: Dispatch< + SetStateAction + >; + setIsCharacterSpecMenuOpen: Dispatch>; + setIsIconSpecMenuOpen: Dispatch>; + setIsPickingCharacterSpecFromCanvas: Dispatch>; + setIsPickingIconSpecFromCanvas: Dispatch>; + 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; +}; + +function triggerPlaceholderAction(label: string) { + window.alert(`${label}功能建设中`); +} + +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); +} + +function resetFailedDialogStatus(dialog: GenerateDialogState) { + return { + ...dialog, + status: dialog.status === 'failed' ? 'idle' : dialog.status, + errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage, + }; +} + +function resetFailedPanelStatus( + panel: T, +) { + return { + ...panel, + status: panel.status === 'failed' ? 'idle' : panel.status, + errorMessage: panel.status === 'failed' ? undefined : panel.errorMessage, + }; +} + +export function ImageCanvasGenerationComposerView({ + specToolWrapRef, + characterSpecButtonRef, + iconSpecButtonRef, + isSpecMenuOpen, + isCharacterSpecMenuOpen, + isIconSpecMenuOpen, + isPickingCharacterSpecFromCanvas, + isPickingIconSpecFromCanvas, + generateDialog, + generationComposerStyle, + iconComposerStyle, + quickEditPanel, + quickEditSourceLayer, + quickEditPanelStyle, + quickEditSizeOptions, + quickEditModelOptions, + characterAnimationPanel, + characterAnimationSourceLayer, + characterAnimationPanelStyle, + characterAnimationPrice, + setGenerateDialog, + setQuickEditPanel, + setCharacterAnimationPanel, + setIsCharacterSpecMenuOpen, + setIsIconSpecMenuOpen, + setIsPickingCharacterSpecFromCanvas, + setIsPickingIconSpecFromCanvas, + onOpenSpecDialog, + onRequestUpload, + onSubmitImageGeneration, + onSubmitIconSpritesheetGeneration, + onSubmitQuickEdit, + onSubmitCharacterAnimation, + onCloseGenerateComposer, + onUpdateSpecFormValue, + onUpdateIconDescription, + onAddIconDescription, + onUpdateCharacterAnimationDuration, +}: ImageCanvasGenerationComposerViewProps) { + return ( + <> + {isSpecMenuOpen + ? renderEditorPortal( + + {(['character', 'ui', 'custom'] as const).map((specType) => ( + onOpenSpecDialog(specType)} + > + {SPEC_TYPE_LABEL[specType]} + + ))} + , + ) + : null} + + {generateDialog?.mode === 'generate' && + generateDialog.composerOpen !== false && + generationComposerStyle ? ( +
event.stopPropagation()} + onSubmit={(event) => { + event.preventDefault(); + if (generateDialog.status !== 'generating') { + onSubmitImageGeneration(generateDialog); + } + }} + > + onRequestUpload('asset')} + icon={} + > + 参考图 + + + setGenerateDialog((currentDialog) => + currentDialog + ? { + ...resetFailedDialogStatus(currentDialog), + prompt: event.target.value, + } + : currentDialog, + ) + } + /> +
+ triggerPlaceholderAction('生成参数')} + trailingIcon={} + > + 中 · 1:1(2k) · 1张 + + triggerPlaceholderAction('模型选择')} + trailingIcon={} + > + GPT Im... + + + {generateDialog.status === 'generating' ? '生成中' : '12'} + +
+ {generateDialog.status === 'generating' ? ( + + 生成中 + + ) : null} + {generateDialog.status === 'failed' ? ( + + {generateDialog.errorMessage} + + ) : null} + + + ) : null} + + {generateDialog?.mode === 'spec' && + generateDialog.composerOpen !== false && + generationComposerStyle ? ( +
event.stopPropagation()} + onSubmit={(event) => { + event.preventDefault(); + if (generateDialog.status !== 'generating') { + onSubmitImageGeneration(generateDialog); + } + }} + > +
+ {generateDialog.specType === 'custom' ? ( + + ) : ( + <> + + + {generateDialog.specType === 'character' ? ( + <> + + + + ) : null} + + )} +
+ {generateDialog.status === 'failed' ? ( + + {generateDialog.errorMessage} + + ) : null} +
+ + {generateDialog.status === 'generating' + ? '生成中' + : `消耗${SPEC_GENERATION_COST}泥点 · 生成`} + +
+
+ ) : null} + + {generateDialog?.mode === 'character' && generationComposerStyle ? ( +
event.stopPropagation()} + onSubmit={(event) => { + event.preventDefault(); + if (generateDialog.status !== 'generating') { + onSubmitImageGeneration(generateDialog); + } + }} + > +
+
+ + 角色形象规范 + + + + +
+ {isCharacterSpecMenuOpen + ? renderEditorPortal( + + { + setIsPickingCharacterSpecFromCanvas(true); + setIsCharacterSpecMenuOpen(false); + }} + > + 从画布中选择 + + { + setIsCharacterSpecMenuOpen(false); + onOpenSpecDialog('character'); + }} + > + 新建角色形象规范 + + { + setIsCharacterSpecMenuOpen(false); + onRequestUpload('character-spec'); + }} + > + 上传图片 + + , + ) + : null} +
+ + 常规参考图 + +
+ {(generateDialog.characterReferences ?? []).map( + (reference, index) => ( + + {reference.label} + + {index + 1} + + + ), + )} + +
+
+
+ + {generateDialog.status === 'failed' ? ( + + {generateDialog.errorMessage} + + ) : null} +
+
+ + 画面比例 + + triggerPlaceholderAction('角色比例')} + > + 1:1 + +
+
+ + 模型 + + triggerPlaceholderAction('角色模型')} + > + GPT Image + +
+ + {generateDialog.status === 'generating' ? '生成中' : '生成'} + +
+
+ ) : null} + + {generateDialog?.mode === 'icon' && + generateDialog.composerOpen !== false && + iconComposerStyle ? ( +
event.stopPropagation()} + onSubmit={(event) => { + event.preventDefault(); + if (generateDialog.status !== 'generating') { + onSubmitIconSpritesheetGeneration(generateDialog); + } + }} + > +
+ + 图标素材规范 + +
+ + + + {isIconSpecMenuOpen + ? renderEditorPortal( + + { + setIsPickingIconSpecFromCanvas(true); + setIsIconSpecMenuOpen(false); + }} + > + 从画布中选择 + + { + setIsIconSpecMenuOpen(false); + onOpenSpecDialog('icon'); + }} + > + 新建图标素材规范 + + { + setIsIconSpecMenuOpen(false); + onRequestUpload('icon-spec'); + }} + > + 上传图片 + + , + ) + : null} +
+ + + +
+
+
+
+ + 素材描述 + +
+ {(generateDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS).map( + (description, index) => ( + + ), + )} +
+
+ {generateDialog.status === 'failed' ? ( + + {generateDialog.errorMessage} + + ) : null} +
+ +
+ + 模型 + + triggerPlaceholderAction('图标模型')} + > + nanobanana2 + +
+ + {generateDialog.status === 'generating' ? '生成中' : '生成'} + +
+
+ ) : null} + + {isPickingCharacterSpecFromCanvas ? ( +
+ 请选择画布中的图片作为角色形象规范,按 Esc 退出 +
+ ) : null} + {isPickingIconSpecFromCanvas ? ( +
+ 请选择画布中的图标素材规范,按 Esc 退出 +
+ ) : null} + + {quickEditPanel && + quickEditPanel.status !== 'generating' && + quickEditSourceLayer && + quickEditPanelStyle ? ( +
event.stopPropagation()} + onSubmit={(event) => { + event.preventDefault(); + onSubmitQuickEdit(); + }} + > +
+
+ {`${quickEditSourceLayer.title}参考图`} + {quickEditSourceLayer.title} +
+ setQuickEditPanel(null)} + /> +
+ + setQuickEditPanel((currentPanel) => + currentPanel + ? { + ...resetFailedPanelStatus(currentPanel), + prompt: event.target.value, + } + : currentPanel, + ) + } + /> +
+ + setQuickEditPanel((currentPanel) => + currentPanel + ? { ...currentPanel, size: event.target.value } + : currentPanel, + ) + } + > + {quickEditSizeOptions.map((size) => ( + + ))} + + + setQuickEditPanel((currentPanel) => + currentPanel + ? { ...currentPanel, model: event.target.value } + : currentPanel, + ) + } + > + {quickEditModelOptions.map((option) => ( + + ))} + +
+ {quickEditPanel.status === 'failed' ? ( + + {quickEditPanel.errorMessage} + + ) : null} + + 生成 + + + ) : null} + + {characterAnimationPanel && + characterAnimationSourceLayer && + characterAnimationPanelStyle ? ( +
event.stopPropagation()} + onSubmit={(event) => { + event.preventDefault(); + if (characterAnimationPanel.status !== 'generating') { + onSubmitCharacterAnimation(); + } + }} + > +
+ 角色动画 + setCharacterAnimationPanel(null)} + /> +
+ + setCharacterAnimationPanel((currentPanel) => + currentPanel + ? { + ...resetFailedPanelStatus(currentPanel), + promptText: event.target.value.slice(0, 4000), + } + : currentPanel, + ) + } + /> +
+ {CHARACTER_ANIMATION_ACTION_PROMPTS.map((preset) => ( + + ))} +
+
+ + setCharacterAnimationPanel((currentPanel) => + currentPanel + ? { + ...resetFailedPanelStatus(currentPanel), + resolution: + event.target.value === '720p' ? '720p' : '480p', + } + : currentPanel, + ) + } + > + + + + + setCharacterAnimationPanel((currentPanel) => + currentPanel + ? { + ...resetFailedPanelStatus(currentPanel), + ratio: + CHARACTER_ANIMATION_RATIO_OPTIONS.find( + (item) => item.value === event.target.value, + )?.value ?? 'same', + } + : currentPanel, + ) + } + > + {CHARACTER_ANIMATION_RATIO_OPTIONS.map((option) => ( + + ))} + + + onUpdateCharacterAnimationDuration(event.target.value) + } + > + {CHARACTER_ANIMATION_DURATION_OPTIONS.map((option) => ( + + ))} + +
+
+ + {characterAnimationPanel.promptText.trim() + ? characterAnimationPanel.promptText.trim() + : '动画描述'} + + {characterAnimationPrice}泥点 +
+ {characterAnimationPanel.status === 'completed' && + characterAnimationPanel.result ? ( + + 已生成 {characterAnimationPanel.result.frameCount} 帧 + + ) : null} + {characterAnimationPanel.status === 'failed' ? ( + + {characterAnimationPanel.errorMessage} + + ) : null} + + {characterAnimationPanel.status === 'generating' + ? '生成中' + : '生成'} + + + ) : null} + + setGenerateDialog(null)} + panelClassName="image-canvas-editor__generate-dialog" + bodyClassName="image-canvas-editor__generate-dialog-body" + > + {generateDialog?.mode === 'edit' ? ( +
{ + event.preventDefault(); + if (generateDialog.status !== 'generating') { + onSubmitImageGeneration(generateDialog); + } + }} + > +
+ + setGenerateDialog((currentDialog) => + currentDialog + ? { + ...currentDialog, + prompt: event.target.value, + } + : currentDialog, + ) + } + /> + {generateDialog.status === 'generating' ? ( + + 修改中 + + ) : null} + {generateDialog.status === 'failed' ? ( + + {generateDialog.errorMessage} + + ) : null} + + {generateDialog.status === 'generating' ? '修改中' : '修改'} + +
+
+ ) : null} +
+ + ); +}