拆分图片画布生成表面编排
新增 useImageCanvasGenerationSurface 收口生成浮层编排。 主视图移除生成 Composer 大段 props 胶水。 舞台控制模型移除重复生成锚点派生。 补充生成表面 hook 单测并更新拆分文档与跟踪记录。
This commit is contained in:
@@ -142,3 +142,4 @@
|
||||
- 2026-06-17 前端拆分第二十四阶段:新增 `useImageCanvasAssetCanvasBridge`,把删除素材清理画布图层、素材加入画布、素材 pointer 拖入画布 / 文件夹、画布 drag/drop 分流和文件拖入画布参数组装从主视图抽成素材到画布桥接 hook;主视图继续保留素材库事实、上传读取、工程资源持久化和历史触发。新增 hook 单测覆盖素材建层、pointer drop 入画布和删除素材清理关联图层 / 选中态;主视图从 1133 行降至 1086 行。验证命令:`npm run test -- src/components/image-editor/useImageCanvasAssetCanvasBridge.test.tsx src/components/image-editor/useImageCanvasAssetPointerDragBridge.test.tsx src/components/image-editor/useImageCanvasCanvasDropWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 清空会话后未登录直接显示 `账号入口`;登录临时开发账号后 `画布背景设置` 是完整设置面板,包含当前色、色域、色相、自定义颜色、预设、HEX 和恢复默认,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,HEX 输入 `#ffffff` 后变为白色,按 Escape 关闭面板;登录后上传图片请求 `/api/editor/assets` 返回 200,素材库出现上传素材,`AI画布工具栏` 保持可见。
|
||||
- 2026-06-17 前端拆分第二十五阶段:新增 `ImageCanvasStageControllerModel` 和 `useImageCanvasStageController`,把舞台派生状态、生成 / 选中浮层位置、右键菜单目标、空白画布右键、图层右键和清空画布焦点从主视图抽出;主视图继续保留工具切换、上传 / 生成入口、图层命令、项目持久化和舞台 pointer 状态机。新增模型和 hook 单测覆盖选中 / 生成锚定、右键菜单位置、显示 / 解锁判断、清空焦点和空白 / 图层右键菜单;主视图从 1086 行降至 993 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasStageControllerModel.test.ts src/components/image-editor/useImageCanvasStageController.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/routing/appPageRoutes.test.ts`、`npm run typecheck`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 清空会话后未登录直接显示 `账号入口`,关闭登录后 `画布背景色` 打开完整 `画布背景设置` dialog,包含色相、自定义颜色、预设、HEX 和恢复默认;点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,`AI画布工具栏` 保持可见,截图留存于 `output/playwright/editor-stage-controller-smoke-20260617.png`。
|
||||
- 2026-06-17 前端拆分第二十六阶段:新增 `ImageCanvasTopbarView`,把返回项目入口、项目标题展示 / 重命名表单、下载画布素材按钮和导出状态提示从主视图抽出;主视图继续保留 chrome hook、项目持久化、导出工作流和实际导出副作用。新增组件单测覆盖返回入口、标题编辑入口、重命名提交 / 取消、导出按钮禁用 / 启用和导出状态提示;主视图从 993 行降至 905 行。验证命令:`npm run test -- src/components/image-editor/ImageCanvasTopbarView.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/useImageCanvasAssetExportWorkflow.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录直接显示 `账号入口`,顶栏返回项目入口、项目名、`画布` 标签和下载按钮均可见;关闭登录后打开 `画布背景设置`,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`,`AI画布工具栏` 保持可见,截图留存于 `output/playwright/editor-topbar-smoke-20260617.png`。
|
||||
- 2026-06-17 前端拆分第二十七阶段:新增 `useImageCanvasGenerationSurface`,把生成 Composer JSX、生成工具切换分流、普通生图 / 图标生成 / 快速编辑 / 角色动画浮层定位从主视图抽出;`useImageCanvasGenerationWorkflow` 继续负责生成状态机和真实 API 提交。同步移除 `ImageCanvasStageControllerModel` 中重复的生成锚点 / Composer 位置派生,避免舞台控制器和生成表面重复持有生成浮层职责;主视图从 905 行降至 793 行。rebase 到远端 `支持规范参考图输入` 后,生成表面继续透传角色形象规范和常规参考图入口,并把左下 dock / 底部工具栏层级提到 Composer 之上,避免生成输入框盖住常用工具和背景面板。验证命令:`npm run test -- src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/ImageCanvasStageControllerModel.test.ts src/components/image-editor/useImageCanvasStageController.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`、`npm run typecheck`、`npm run check:encoding`、`git diff --check`;浏览器回归:`http://127.0.0.1:10007/editor/canvas` 未登录直接显示 `账号入口`,关闭登录后 `画布背景设置` 保持完整面板,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,再点击 `生成角色形象` 能打开包含 `角色形象规范` 和 `常规参考图` 的对话框,控制台仅有未登录 refresh 401。
|
||||
|
||||
@@ -210,9 +210,9 @@
|
||||
## 第二十五阶段模块
|
||||
|
||||
- `ImageCanvasStageControllerModel.ts`
|
||||
- 承载舞台派生状态和右键菜单模型:选中图层、生成对象锚点、生成输入框位置、选中浮动工具栏位置、图片菜单图层、右键菜单目标图层,以及显示 / 解锁菜单文案判断。
|
||||
- 该模型复用既有图层命令模型与浮层定位模型,不重新实现坐标公式,避免拆分后出现第二套右键目标和浮层定位规则。
|
||||
- 新增单测覆盖生成锚定、选中工具栏位置、右键目标集合、显示 / 解锁判断和菜单位置限制。
|
||||
- 承载舞台派生状态和右键菜单模型:选中图层、选中浮动工具栏位置、图片菜单图层、右键菜单目标图层,以及显示 / 解锁菜单文案判断。
|
||||
- 该模型复用既有图层命令模型与浮层定位模型,不重新实现右键目标和选中工具栏坐标规则;生成 Composer 锚点不属于舞台控制器,后续由生成表面编排统一负责。
|
||||
- 新增单测覆盖选中工具栏位置、右键目标集合、显示 / 解锁判断和菜单位置限制。
|
||||
|
||||
- `useImageCanvasStageController.ts`
|
||||
- 承载舞台控制胶水:清空画布焦点、空白画布右键菜单和图层右键菜单处理。
|
||||
@@ -227,6 +227,14 @@
|
||||
- 新增组件单测覆盖返回入口、标题编辑入口、重命名提交 / 取消、导出按钮禁用 / 启用和导出状态提示。
|
||||
- 本阶段主视图从 993 行降至 905 行;后续可继续评估隐藏上传 input / 侧栏拖拽预览这类根级浮层是否值得抽为编辑器 shell 视图。
|
||||
|
||||
## 第二十七阶段模块
|
||||
|
||||
- `useImageCanvasGenerationSurface.tsx`
|
||||
- 承载 Lovart 式生成表面编排:组合 `useImageCanvasGenerationWorkflow`,统一计算普通生图、图标生成、快速编辑和角色动画的浮层位置,并构建 `ImageCanvasGenerationComposerView` 节点。
|
||||
- `useImageCanvasGenerationWorkflow` 继续作为生成状态机和真实 API 提交入口;生成表面 hook 只收口 Composer JSX、工具切换分流和浮层定位,避免主视图继续维护大段生成 props 胶水。
|
||||
- 本阶段同步把生成 Composer 锚点从 `ImageCanvasStageControllerModel` 移出,避免舞台控制器和生成表面各自计算一套生成浮层状态。
|
||||
- 新增 hook 单测覆盖生成工具切换、Composer 位置、关闭生成输入框和规范菜单;主视图从 905 行降至 793 行。
|
||||
|
||||
## 后续阶段
|
||||
|
||||
- 后续可继续选择更高内聚的交互 workflow 或持久化边界,不再把生成链路继续拆成浅层 wrapper。
|
||||
@@ -234,7 +242,7 @@
|
||||
|
||||
## 验证计划
|
||||
|
||||
- `npm run test -- src/components/image-editor/ImageCanvasTopbarView.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/useImageCanvasAssetExportWorkflow.test.tsx src/components/image-editor/useImageCanvasEditorChrome.test.tsx`
|
||||
- `npm run test -- src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/ImageCanvasStageControllerModel.test.ts src/components/image-editor/useImageCanvasStageController.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorView.test.tsx`
|
||||
- `npm run typecheck`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { ImageCanvasGenerationComposerView } from './ImageCanvasGenerationComposerView';
|
||||
import { ImageCanvasMetadataModalView } from './ImageCanvasMetadataModalView';
|
||||
import { ImageCanvasSidebarView } from './ImageCanvasSidebarView';
|
||||
import { ImageCanvasStageView } from './ImageCanvasStageView';
|
||||
import { ImageCanvasTopbarView } from './ImageCanvasTopbarView';
|
||||
import { resolveContextMenuPosition } from './ImageCanvasEditorModel';
|
||||
import {
|
||||
isCanvasGenerationComposerVisible,
|
||||
resolveCharacterAnimationPanelStyle,
|
||||
resolveIconComposerStyle,
|
||||
resolveQuickEditPanelStyle,
|
||||
} from './ImageCanvasOverlayModel';
|
||||
import { isCanvasGenerationComposerVisible } from './ImageCanvasOverlayModel';
|
||||
import type {
|
||||
AssetPointerDragState,
|
||||
CanvasContextMenuState,
|
||||
@@ -30,7 +24,7 @@ import {
|
||||
} from './useImageCanvasAssetCanvasBridge';
|
||||
import { useImageCanvasAssetExportWorkflow } from './useImageCanvasAssetExportWorkflow';
|
||||
import { useImageCanvasEditorChrome } from './useImageCanvasEditorChrome';
|
||||
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
|
||||
import { useImageCanvasGenerationSurface } from './useImageCanvasGenerationSurface';
|
||||
import { useImageCanvasKeyboardShortcuts } from './useImageCanvasKeyboardShortcuts';
|
||||
import { useImageCanvasLayerCommands } from './useImageCanvasLayerCommands';
|
||||
import { useImageCanvasProjectPersistence } from './useImageCanvasProjectPersistence';
|
||||
@@ -330,13 +324,44 @@ export function ImageCanvasEditorView() {
|
||||
projectId,
|
||||
projectTitle,
|
||||
});
|
||||
const generationWorkflow = useImageCanvasGenerationWorkflow({
|
||||
const {
|
||||
uploadInputRef,
|
||||
setUploadTarget,
|
||||
requestUpload,
|
||||
handleUploadInputChange,
|
||||
addUploadedFiles,
|
||||
} = useImageCanvasUploadWorkflow({
|
||||
canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true,
|
||||
openEditorLoginModal,
|
||||
assetFolders,
|
||||
activeUploadFolderId,
|
||||
canvasSize,
|
||||
viewport,
|
||||
activeTool,
|
||||
allocateUploadIndex: () => {
|
||||
layerCounterRef.current += 1;
|
||||
return layerCounterRef.current;
|
||||
},
|
||||
setAssetFolders,
|
||||
setAssets,
|
||||
setLayers,
|
||||
setGenerateDialog,
|
||||
setActiveSidebarPanel,
|
||||
appendCanvasLayersWithResources,
|
||||
selectSingleLayer,
|
||||
});
|
||||
const generationSurface = useImageCanvasGenerationSurface({
|
||||
layers,
|
||||
canvasSize,
|
||||
viewport,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
activeCanvasGenerationDialog,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogById,
|
||||
@@ -349,58 +374,35 @@ export function ImageCanvasEditorView() {
|
||||
setActiveSidebarPanel,
|
||||
setMetadataLayer,
|
||||
setImageContextMenu,
|
||||
requestUpload,
|
||||
});
|
||||
const {
|
||||
quickEditPanel,
|
||||
setQuickEditPanel,
|
||||
quickEditSourceLayer,
|
||||
quickEditSizeOptions,
|
||||
quickEditModelOptions,
|
||||
characterAnimationPanel,
|
||||
setCharacterAnimationPanel,
|
||||
characterAnimationSourceLayer,
|
||||
characterAnimationPrice,
|
||||
iconDescriptionValues,
|
||||
isSpecMenuOpen,
|
||||
setIsSpecMenuOpen,
|
||||
isCharacterSpecMenuOpen,
|
||||
setIsCharacterSpecMenuOpen,
|
||||
isCharacterReferenceMenuOpen,
|
||||
setIsCharacterReferenceMenuOpen,
|
||||
isPickingCharacterSpecFromCanvas,
|
||||
setIsPickingCharacterSpecFromCanvas,
|
||||
isPickingCharacterReferenceFromCanvas,
|
||||
setIsPickingCharacterReferenceFromCanvas,
|
||||
isIconSpecMenuOpen,
|
||||
setIsIconSpecMenuOpen,
|
||||
isPickingIconSpecFromCanvas,
|
||||
setIsPickingIconSpecFromCanvas,
|
||||
openGenerateDialog,
|
||||
openSpecDialog,
|
||||
openCharacterAnimationPanel,
|
||||
openCharacterGenerationDialog,
|
||||
openIconGenerationDialog,
|
||||
openEditDialog,
|
||||
openQuickEditPanel,
|
||||
pickCharacterSpecFromLayer,
|
||||
pickCharacterReferenceFromLayer,
|
||||
pickIconSpecFromLayer,
|
||||
submitIconSpritesheetGeneration,
|
||||
submitQuickEdit,
|
||||
submitImageGeneration,
|
||||
updateSpecFormValue,
|
||||
updateIconDescription,
|
||||
addIconDescription,
|
||||
updateCharacterAnimationDuration,
|
||||
rememberImageModel,
|
||||
submitCharacterAnimation,
|
||||
hideGeneratedLayerPanelAfterBlur,
|
||||
closeGenerateComposer,
|
||||
clearDeletedLayerGenerationState,
|
||||
} = generationWorkflow;
|
||||
generationComposerStyle,
|
||||
generationComposerNode,
|
||||
switchGenerationTool,
|
||||
} = generationSurface;
|
||||
const {
|
||||
selectedLayer,
|
||||
generationComposerStyle,
|
||||
selectedToolbarStyle,
|
||||
imageContextMenuLayer,
|
||||
contextShouldShowLayer,
|
||||
@@ -412,7 +414,6 @@ export function ImageCanvasEditorView() {
|
||||
layers,
|
||||
selectedLayerId,
|
||||
selectedLayerIds,
|
||||
activeCanvasGenerationDialog,
|
||||
imageContextMenu,
|
||||
setImageContextMenu,
|
||||
contextMenu,
|
||||
@@ -423,23 +424,6 @@ export function ImageCanvasEditorView() {
|
||||
hideGeneratedLayerPanelAfterBlur,
|
||||
getCanvasPointFromClient,
|
||||
});
|
||||
const iconComposerStyle = resolveIconComposerStyle({
|
||||
dialog: activeCanvasGenerationDialog,
|
||||
composerStyle: generationComposerStyle,
|
||||
iconDescriptionCount: iconDescriptionValues.length,
|
||||
});
|
||||
const quickEditPanelStyle = resolveQuickEditPanelStyle({
|
||||
panel: quickEditPanel,
|
||||
sourceLayer: quickEditSourceLayer,
|
||||
viewport,
|
||||
canvasSize,
|
||||
});
|
||||
const characterAnimationPanelStyle = resolveCharacterAnimationPanelStyle({
|
||||
panel: characterAnimationPanel,
|
||||
sourceLayer: characterAnimationSourceLayer,
|
||||
viewport,
|
||||
canvasSize,
|
||||
});
|
||||
const {
|
||||
canvasClipboard,
|
||||
pasteCanvasClipboard,
|
||||
@@ -474,32 +458,6 @@ export function ImageCanvasEditorView() {
|
||||
onDeleteLayerSideEffects: clearDeletedLayerGenerationState,
|
||||
exportLayerImage,
|
||||
});
|
||||
const {
|
||||
uploadInputRef,
|
||||
setUploadTarget,
|
||||
requestUpload,
|
||||
handleUploadInputChange,
|
||||
addUploadedFiles,
|
||||
} = useImageCanvasUploadWorkflow({
|
||||
canAccessProtectedData: authUi ? authUi.canAccessProtectedData : true,
|
||||
openEditorLoginModal,
|
||||
assetFolders,
|
||||
activeUploadFolderId,
|
||||
canvasSize,
|
||||
viewport,
|
||||
activeTool,
|
||||
allocateUploadIndex: () => {
|
||||
layerCounterRef.current += 1;
|
||||
return layerCounterRef.current;
|
||||
},
|
||||
setAssetFolders,
|
||||
setAssets,
|
||||
setLayers,
|
||||
setGenerateDialog,
|
||||
setActiveSidebarPanel,
|
||||
appendCanvasLayersWithResources,
|
||||
selectSingleLayer,
|
||||
});
|
||||
const {
|
||||
canvasMarquee,
|
||||
isPanning,
|
||||
@@ -627,21 +585,7 @@ export function ImageCanvasEditorView() {
|
||||
requestUpload('asset');
|
||||
return;
|
||||
}
|
||||
if (tool === 'generate') {
|
||||
openGenerateDialog();
|
||||
return;
|
||||
}
|
||||
if (tool === 'spec') {
|
||||
setIsSpecMenuOpen((open) => !open);
|
||||
setActiveTool('spec');
|
||||
return;
|
||||
}
|
||||
if (tool === 'character') {
|
||||
openCharacterGenerationDialog();
|
||||
return;
|
||||
}
|
||||
if (tool === 'icon') {
|
||||
openIconGenerationDialog();
|
||||
if (switchGenerationTool(tool)) {
|
||||
return;
|
||||
}
|
||||
setActiveTool(tool);
|
||||
@@ -844,74 +788,7 @@ export function ImageCanvasEditorView() {
|
||||
onMinimapPointerDown={handleMinimapPointerDown}
|
||||
onSwitchTool={switchTool}
|
||||
>
|
||||
<ImageCanvasGenerationComposerView
|
||||
specToolWrapRef={specToolWrapRef}
|
||||
characterSpecButtonRef={characterSpecButtonRef}
|
||||
characterReferenceButtonRef={characterReferenceButtonRef}
|
||||
iconSpecButtonRef={iconSpecButtonRef}
|
||||
isSpecMenuOpen={isSpecMenuOpen}
|
||||
isCharacterSpecMenuOpen={isCharacterSpecMenuOpen}
|
||||
isCharacterReferenceMenuOpen={isCharacterReferenceMenuOpen}
|
||||
isIconSpecMenuOpen={isIconSpecMenuOpen}
|
||||
isPickingCharacterSpecFromCanvas={isPickingCharacterSpecFromCanvas}
|
||||
isPickingCharacterReferenceFromCanvas={
|
||||
isPickingCharacterReferenceFromCanvas
|
||||
}
|
||||
isPickingIconSpecFromCanvas={isPickingIconSpecFromCanvas}
|
||||
generateDialog={generateDialog}
|
||||
generationComposerStyle={generationComposerStyle}
|
||||
iconComposerStyle={iconComposerStyle}
|
||||
quickEditPanel={quickEditPanel}
|
||||
quickEditSourceLayer={quickEditSourceLayer}
|
||||
quickEditPanelStyle={quickEditPanelStyle}
|
||||
quickEditSizeOptions={quickEditSizeOptions}
|
||||
quickEditModelOptions={quickEditModelOptions}
|
||||
characterAnimationPanel={characterAnimationPanel}
|
||||
characterAnimationSourceLayer={characterAnimationSourceLayer}
|
||||
characterAnimationPanelStyle={characterAnimationPanelStyle}
|
||||
characterAnimationPrice={characterAnimationPrice}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
setQuickEditPanel={setQuickEditPanel}
|
||||
setCharacterAnimationPanel={setCharacterAnimationPanel}
|
||||
setIsCharacterSpecMenuOpen={setIsCharacterSpecMenuOpen}
|
||||
setIsCharacterReferenceMenuOpen={setIsCharacterReferenceMenuOpen}
|
||||
setIsIconSpecMenuOpen={setIsIconSpecMenuOpen}
|
||||
setIsPickingCharacterSpecFromCanvas={
|
||||
setIsPickingCharacterSpecFromCanvas
|
||||
}
|
||||
setIsPickingCharacterReferenceFromCanvas={
|
||||
setIsPickingCharacterReferenceFromCanvas
|
||||
}
|
||||
setIsPickingIconSpecFromCanvas={setIsPickingIconSpecFromCanvas}
|
||||
onOpenSpecDialog={openSpecDialog}
|
||||
onRequestUpload={requestUpload}
|
||||
onSubmitImageGeneration={(dialog) =>
|
||||
void submitImageGeneration(dialog)
|
||||
}
|
||||
onSubmitIconSpritesheetGeneration={(dialog) =>
|
||||
void submitIconSpritesheetGeneration(dialog)
|
||||
}
|
||||
onSubmitQuickEdit={() => void submitQuickEdit()}
|
||||
onSubmitCharacterAnimation={() => void submitCharacterAnimation()}
|
||||
onCloseGenerateComposer={() => {
|
||||
setGenerateDialog((currentDialog) =>
|
||||
currentDialog?.mode === 'generate'
|
||||
? {
|
||||
...currentDialog,
|
||||
composerOpen: false,
|
||||
}
|
||||
: currentDialog,
|
||||
);
|
||||
setActiveTool('select');
|
||||
}}
|
||||
onUpdateSpecFormValue={updateSpecFormValue}
|
||||
onUpdateIconDescription={updateIconDescription}
|
||||
onAddIconDescription={addIconDescription}
|
||||
onUpdateCharacterAnimationDuration={
|
||||
updateCharacterAnimationDuration
|
||||
}
|
||||
onRememberImageModel={rememberImageModel}
|
||||
/>
|
||||
{generationComposerNode}
|
||||
</ImageCanvasStageView>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type {
|
||||
CanvasContextMenuState,
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
@@ -32,28 +31,8 @@ function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
};
|
||||
}
|
||||
|
||||
function createDialog(
|
||||
overrides: Partial<CanvasGenerationDialogState> = {},
|
||||
): CanvasGenerationDialogState {
|
||||
return {
|
||||
id: 'dialog-a',
|
||||
mode: 'generate',
|
||||
prompt: '',
|
||||
status: 'idle',
|
||||
placeholder: {
|
||||
x: 300,
|
||||
y: 200,
|
||||
width: 360,
|
||||
height: 260,
|
||||
originalWidth: 1024,
|
||||
originalHeight: 1024,
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('ImageCanvasStageControllerModel', () => {
|
||||
it('derives selected layer, generation anchor, and overlay positions', () => {
|
||||
it('derives selected layer, selected toolbar, and context image target', () => {
|
||||
const selectedLayer = createLayer({ id: 'selected', x: 40, y: 60 });
|
||||
const generatedLayer = createLayer({
|
||||
id: 'generated',
|
||||
@@ -62,13 +41,11 @@ describe('ImageCanvasStageControllerModel', () => {
|
||||
width: 320,
|
||||
height: 180,
|
||||
});
|
||||
const dialog = createDialog({ generatedLayerId: generatedLayer.id });
|
||||
|
||||
const model = resolveImageCanvasStageControllerModel({
|
||||
layers: [selectedLayer, generatedLayer],
|
||||
selectedLayerId: selectedLayer.id,
|
||||
selectedLayerIds: [selectedLayer.id, generatedLayer.id],
|
||||
activeCanvasGenerationDialog: dialog,
|
||||
imageContextMenu: { layerId: generatedLayer.id, x: 0, y: 0 },
|
||||
contextMenu: null,
|
||||
viewport: { x: 10, y: 20, scale: 2 },
|
||||
@@ -78,9 +55,6 @@ describe('ImageCanvasStageControllerModel', () => {
|
||||
expect(model.selectedLayer).toBe(selectedLayer);
|
||||
expect(model.selectedLayerCount).toBe(2);
|
||||
expect(model.hasMultipleSelectedLayers).toBe(true);
|
||||
expect(model.activeGenerationLayer).toBe(generatedLayer);
|
||||
expect(model.generationAnchor).toBe(generatedLayer);
|
||||
expect(model.generationComposerStyle).toEqual({ left: 730, top: 710 });
|
||||
expect(model.selectedToolbarStyle).toEqual({ left: 330, top: 128 });
|
||||
expect(model.imageContextMenuLayer).toBe(generatedLayer);
|
||||
});
|
||||
@@ -100,7 +74,6 @@ describe('ImageCanvasStageControllerModel', () => {
|
||||
layers: [visibleLayer, hiddenLayer],
|
||||
selectedLayerId: visibleLayer.id,
|
||||
selectedLayerIds: [visibleLayer.id, hiddenLayer.id],
|
||||
activeCanvasGenerationDialog: null,
|
||||
imageContextMenu: null,
|
||||
contextMenu,
|
||||
viewport: { x: 0, y: 0, scale: 1 },
|
||||
|
||||
@@ -7,14 +7,11 @@ import {
|
||||
} from './ImageCanvasEditorModel';
|
||||
import type {
|
||||
CanvasContextMenuState,
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
CanvasViewport,
|
||||
ImageContextMenuState,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
resolveGenerationAnchor,
|
||||
resolveGenerationComposerStyle,
|
||||
resolveSelectedToolbarStyle,
|
||||
} from './ImageCanvasOverlayModel';
|
||||
|
||||
@@ -22,9 +19,6 @@ export type ImageCanvasStageControllerModel = {
|
||||
selectedLayer: CanvasLayer | null;
|
||||
selectedLayerCount: number;
|
||||
hasMultipleSelectedLayers: boolean;
|
||||
activeGenerationLayer: CanvasLayer | null;
|
||||
generationAnchor: CanvasLayer | CanvasGenerationDialogState['placeholder'] | null;
|
||||
generationComposerStyle: ReturnType<typeof resolveGenerationComposerStyle>;
|
||||
selectedToolbarStyle: ReturnType<typeof resolveSelectedToolbarStyle>;
|
||||
imageContextMenuLayer: CanvasLayer | null;
|
||||
contextTargetIds: string[];
|
||||
@@ -37,7 +31,6 @@ type ResolveImageCanvasStageControllerModelOptions = {
|
||||
layers: CanvasLayer[];
|
||||
selectedLayerId: string | null;
|
||||
selectedLayerIds: string[];
|
||||
activeCanvasGenerationDialog: CanvasGenerationDialogState | null;
|
||||
imageContextMenu: ImageContextMenuState | null;
|
||||
contextMenu: CanvasContextMenuState | null;
|
||||
viewport: CanvasViewport;
|
||||
@@ -48,7 +41,6 @@ export function resolveImageCanvasStageControllerModel({
|
||||
layers,
|
||||
selectedLayerId,
|
||||
selectedLayerIds,
|
||||
activeCanvasGenerationDialog,
|
||||
imageContextMenu,
|
||||
contextMenu,
|
||||
viewport,
|
||||
@@ -57,18 +49,6 @@ export function resolveImageCanvasStageControllerModel({
|
||||
const selectedLayer =
|
||||
layers.find((layer) => layer.id === selectedLayerId) ?? null;
|
||||
const selectedLayerCount = selectedLayerIds.length;
|
||||
const activeGenerationLayer =
|
||||
activeCanvasGenerationDialog?.generatedLayerId
|
||||
? (layers.find(
|
||||
(layer) => layer.id === activeCanvasGenerationDialog.generatedLayerId,
|
||||
) ?? null)
|
||||
: null;
|
||||
const generationAnchor = activeCanvasGenerationDialog
|
||||
? resolveGenerationAnchor({
|
||||
dialog: activeCanvasGenerationDialog,
|
||||
generatedLayer: activeGenerationLayer,
|
||||
})
|
||||
: null;
|
||||
const imageContextMenuLayer = imageContextMenu
|
||||
? (layers.find((layer) => layer.id === imageContextMenu.layerId) ?? null)
|
||||
: null;
|
||||
@@ -82,13 +62,6 @@ export function resolveImageCanvasStageControllerModel({
|
||||
selectedLayer,
|
||||
selectedLayerCount,
|
||||
hasMultipleSelectedLayers: selectedLayerCount > 1,
|
||||
activeGenerationLayer,
|
||||
generationAnchor,
|
||||
generationComposerStyle: resolveGenerationComposerStyle({
|
||||
dialog: activeCanvasGenerationDialog,
|
||||
anchor: generationAnchor,
|
||||
viewport,
|
||||
}),
|
||||
selectedToolbarStyle: resolveSelectedToolbarStyle({
|
||||
selectedLayer,
|
||||
viewport,
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { fireEvent, render, screen, within } from '@testing-library/react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
CanvasTool,
|
||||
GenerateDialogState,
|
||||
ImageContextMenuState,
|
||||
SidebarPanel,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { useCanvasGenerationDialogs } from './useCanvasGenerationDialogs';
|
||||
import { useImageCanvasGenerationSurface } from './useImageCanvasGenerationSurface';
|
||||
|
||||
vi.mock('../../services/image-editor/editorImageReference', () => ({
|
||||
resolveEditorImageReferenceDataUrl: vi.fn(async (src: string) => src),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/image-editor/editorProjectClient', async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import('../../services/image-editor/editorProjectClient')
|
||||
>('../../services/image-editor/editorProjectClient');
|
||||
return {
|
||||
...actual,
|
||||
editEditorImage: vi.fn(),
|
||||
generateEditorCharacterAnimation: vi.fn(),
|
||||
generateEditorIconSpritesheet: vi.fn(),
|
||||
generateEditorImage: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||
const id = overrides.id ?? 'layer-a';
|
||||
return {
|
||||
id,
|
||||
resourceId: `resource-${id}`,
|
||||
title: id,
|
||||
src: `data:image/png;base64,${id}`,
|
||||
x: 80,
|
||||
y: 90,
|
||||
width: 240,
|
||||
height: 180,
|
||||
originalWidth: 240,
|
||||
originalHeight: 180,
|
||||
zIndex: 1,
|
||||
sourceType: 'uploaded',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function GenerationSurfaceHarness() {
|
||||
const [layers, setLayers] = useState<CanvasLayer[]>([createLayer()]);
|
||||
const [activeTool, setActiveTool] = useState<CanvasTool>('select');
|
||||
const [activeSidebarPanel, setActiveSidebarPanel] =
|
||||
useState<SidebarPanel | null>('assets');
|
||||
const [, setMetadataLayer] = useState<CanvasLayer | null>(null);
|
||||
const [, setImageContextMenu] = useState<ImageContextMenuState | null>(null);
|
||||
const layerCounterRef = useRef(0);
|
||||
const specToolWrapRef = useRef<HTMLSpanElement | null>(null);
|
||||
const characterSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const characterReferenceButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const iconSpecButtonRef = useRef<HTMLButtonElement | null>(null);
|
||||
const dialogs = useCanvasGenerationDialogs();
|
||||
const activeDialog = dialogs.generateDialog;
|
||||
const activeCanvasDialog =
|
||||
activeDialog && 'id' in activeDialog
|
||||
? (activeDialog as CanvasGenerationDialogState)
|
||||
: null;
|
||||
const surface = useImageCanvasGenerationSurface({
|
||||
layers,
|
||||
canvasSize: { width: 900, height: 640 },
|
||||
viewport: { x: 10, y: 20, scale: 2 },
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
generateDialog: dialogs.generateDialog,
|
||||
setGenerateDialog: dialogs.setGenerateDialog,
|
||||
activeCanvasGenerationDialog: activeCanvasDialog,
|
||||
openCanvasGenerationDialog: dialogs.openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById: dialogs.updateCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogById: dialogs.removeCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogsByLayerId:
|
||||
dialogs.removeCanvasGenerationDialogsByLayerId,
|
||||
getGeneratingDialogPlaceholder: dialogs.getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources: (nextLayers) =>
|
||||
setLayers((currentLayers) => [...currentLayers, ...nextLayers]),
|
||||
selectSingleLayer: () => {},
|
||||
fitLayers: () => {},
|
||||
setActiveTool,
|
||||
setActiveSidebarPanel,
|
||||
setMetadataLayer,
|
||||
setImageContextMenu,
|
||||
requestUpload: () => {},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<span ref={specToolWrapRef}>规范入口</span>
|
||||
<span data-testid="tool">{activeTool}</span>
|
||||
<span data-testid="sidebar">{activeSidebarPanel ?? '-'}</span>
|
||||
<span data-testid="dialog">
|
||||
{activeDialog
|
||||
? `${activeDialog.mode}:${activeDialog.composerOpen !== false ? 'open' : 'closed'}:${activeDialog.placeholder ? 'placeholder' : '-'}`
|
||||
: '-'}
|
||||
</span>
|
||||
<span data-testid="composer-left">
|
||||
{surface.generationComposerStyle?.left ?? '-'}
|
||||
</span>
|
||||
<span data-testid="composer-top">
|
||||
{surface.generationComposerStyle?.top ?? '-'}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => surface.switchGenerationTool('generate')}
|
||||
>
|
||||
切换生成
|
||||
</button>
|
||||
<button type="button" onClick={() => surface.switchGenerationTool('spec')}>
|
||||
切换规范
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => surface.switchGenerationTool('character')}
|
||||
>
|
||||
切换角色
|
||||
</button>
|
||||
<button type="button" onClick={() => surface.switchGenerationTool('icon')}>
|
||||
切换图标
|
||||
</button>
|
||||
<button type="button" onClick={() => surface.switchGenerationTool('text')}>
|
||||
切换文字
|
||||
</button>
|
||||
{surface.generationComposerNode}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
describe('useImageCanvasGenerationSurface', () => {
|
||||
it('switches generation tools while leaving non-generation tools untouched', () => {
|
||||
render(<GenerationSurfaceHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '切换文字' }));
|
||||
expect(screen.getByTestId('tool').textContent).toBe('select');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '切换生成' }));
|
||||
expect(screen.getByTestId('tool').textContent).toBe('generate');
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'generate:open:placeholder',
|
||||
);
|
||||
expect(screen.getByRole('dialog', { name: '生成图片' })).toBeTruthy();
|
||||
});
|
||||
|
||||
it('derives composer position and closes the generated image composer', () => {
|
||||
render(<GenerationSurfaceHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '切换生成' }));
|
||||
expect(screen.getByTestId('composer-left').textContent).toBe('450');
|
||||
expect(screen.getByTestId('composer-top').textContent).toBe('750');
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '关闭生成图片' }));
|
||||
expect(screen.queryByRole('dialog', { name: '生成图片' })).toBeNull();
|
||||
expect(screen.getByTestId('tool').textContent).toBe('select');
|
||||
expect(screen.getByTestId('dialog').textContent).toBe(
|
||||
'generate:closed:placeholder',
|
||||
);
|
||||
});
|
||||
|
||||
it('opens the spec menu through the generation surface', () => {
|
||||
render(<GenerationSurfaceHarness />);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '切换规范' }));
|
||||
expect(screen.getByTestId('tool').textContent).toBe('spec');
|
||||
const menu = screen.getByRole('menu', { name: '生成规范类型' });
|
||||
expect(within(menu).getByRole('menuitem', { name: '角色形象规范' })).toBeTruthy();
|
||||
});
|
||||
});
|
||||
256
src/components/image-editor/useImageCanvasGenerationSurface.tsx
Normal file
256
src/components/image-editor/useImageCanvasGenerationSurface.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
type Dispatch,
|
||||
type MutableRefObject,
|
||||
type RefObject,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { ImageCanvasGenerationComposerView } from './ImageCanvasGenerationComposerView';
|
||||
import {
|
||||
resolveCharacterAnimationPanelStyle,
|
||||
resolveGenerationAnchor,
|
||||
resolveGenerationComposerStyle,
|
||||
resolveIconComposerStyle,
|
||||
resolveQuickEditPanelStyle,
|
||||
} from './ImageCanvasOverlayModel';
|
||||
import type {
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
CanvasTool,
|
||||
CanvasViewport,
|
||||
GenerateDialogState,
|
||||
ImageContextMenuState,
|
||||
SidebarPanel,
|
||||
UploadTarget,
|
||||
} from './ImageCanvasEditorTypes';
|
||||
import { useImageCanvasGenerationWorkflow } from './useImageCanvasGenerationWorkflow';
|
||||
|
||||
type CanvasGenerationDialogUpdater = (
|
||||
dialog: CanvasGenerationDialogState,
|
||||
) => CanvasGenerationDialogState | null;
|
||||
|
||||
type ImageCanvasGenerationSurfaceOptions = {
|
||||
layers: CanvasLayer[];
|
||||
canvasSize: { width: number; height: number };
|
||||
viewport: CanvasViewport;
|
||||
layerCounterRef: MutableRefObject<number>;
|
||||
specToolWrapRef: RefObject<HTMLSpanElement | null>;
|
||||
characterSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
characterReferenceButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
iconSpecButtonRef: RefObject<HTMLButtonElement | null>;
|
||||
generateDialog: GenerateDialogState | null;
|
||||
setGenerateDialog: Dispatch<SetStateAction<GenerateDialogState | null>>;
|
||||
activeCanvasGenerationDialog: CanvasGenerationDialogState | null;
|
||||
openCanvasGenerationDialog: (
|
||||
dialog: Omit<CanvasGenerationDialogState, 'id'>,
|
||||
) => void;
|
||||
updateCanvasGenerationDialogById: (
|
||||
dialogId: string,
|
||||
updater: CanvasGenerationDialogUpdater,
|
||||
) => void;
|
||||
removeCanvasGenerationDialogById: (dialogId: string) => void;
|
||||
removeCanvasGenerationDialogsByLayerId: (targetLayerId: string) => void;
|
||||
getGeneratingDialogPlaceholder: (
|
||||
dialog: GenerateDialogState,
|
||||
) => GenerateDialogState['placeholder'];
|
||||
appendCanvasLayersWithResources: (nextLayers: CanvasLayer[]) => void;
|
||||
selectSingleLayer: (layerId: string | null) => void;
|
||||
fitLayers: (targetLayers?: CanvasLayer[]) => void;
|
||||
setActiveTool: Dispatch<SetStateAction<CanvasTool>>;
|
||||
setActiveSidebarPanel: Dispatch<SetStateAction<SidebarPanel | null>>;
|
||||
setMetadataLayer: Dispatch<SetStateAction<CanvasLayer | null>>;
|
||||
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
|
||||
requestUpload: (target: UploadTarget) => void;
|
||||
};
|
||||
|
||||
export function useImageCanvasGenerationSurface({
|
||||
layers,
|
||||
canvasSize,
|
||||
viewport,
|
||||
layerCounterRef,
|
||||
specToolWrapRef,
|
||||
characterSpecButtonRef,
|
||||
characterReferenceButtonRef,
|
||||
iconSpecButtonRef,
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
activeCanvasGenerationDialog,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogsByLayerId,
|
||||
getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources,
|
||||
selectSingleLayer,
|
||||
fitLayers,
|
||||
setActiveTool,
|
||||
setActiveSidebarPanel,
|
||||
setMetadataLayer,
|
||||
setImageContextMenu,
|
||||
requestUpload,
|
||||
}: ImageCanvasGenerationSurfaceOptions) {
|
||||
const generationWorkflow = useImageCanvasGenerationWorkflow({
|
||||
layers,
|
||||
canvasSize,
|
||||
viewport,
|
||||
layerCounterRef,
|
||||
generateDialog,
|
||||
setGenerateDialog,
|
||||
openCanvasGenerationDialog,
|
||||
updateCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogById,
|
||||
removeCanvasGenerationDialogsByLayerId,
|
||||
getGeneratingDialogPlaceholder,
|
||||
appendCanvasLayersWithResources,
|
||||
selectSingleLayer,
|
||||
fitLayers,
|
||||
setActiveTool,
|
||||
setActiveSidebarPanel,
|
||||
setMetadataLayer,
|
||||
setImageContextMenu,
|
||||
});
|
||||
|
||||
const activeGenerationLayer =
|
||||
activeCanvasGenerationDialog?.generatedLayerId
|
||||
? (layers.find(
|
||||
(layer) => layer.id === activeCanvasGenerationDialog.generatedLayerId,
|
||||
) ?? null)
|
||||
: null;
|
||||
const generationAnchor = resolveGenerationAnchor({
|
||||
dialog: activeCanvasGenerationDialog,
|
||||
generatedLayer: activeGenerationLayer,
|
||||
});
|
||||
const generationComposerStyle = resolveGenerationComposerStyle({
|
||||
dialog: activeCanvasGenerationDialog,
|
||||
anchor: generationAnchor,
|
||||
viewport,
|
||||
});
|
||||
const iconComposerStyle = resolveIconComposerStyle({
|
||||
dialog: activeCanvasGenerationDialog,
|
||||
composerStyle: generationComposerStyle,
|
||||
iconDescriptionCount: generationWorkflow.iconDescriptionValues.length,
|
||||
});
|
||||
const quickEditPanelStyle = resolveQuickEditPanelStyle({
|
||||
panel: generationWorkflow.quickEditPanel,
|
||||
sourceLayer: generationWorkflow.quickEditSourceLayer,
|
||||
viewport,
|
||||
canvasSize,
|
||||
});
|
||||
const characterAnimationPanelStyle = resolveCharacterAnimationPanelStyle({
|
||||
panel: generationWorkflow.characterAnimationPanel,
|
||||
sourceLayer: generationWorkflow.characterAnimationSourceLayer,
|
||||
viewport,
|
||||
canvasSize,
|
||||
});
|
||||
|
||||
const switchGenerationTool = useCallback(
|
||||
(tool: CanvasTool) => {
|
||||
if (tool === 'generate') {
|
||||
generationWorkflow.openGenerateDialog();
|
||||
return true;
|
||||
}
|
||||
if (tool === 'spec') {
|
||||
generationWorkflow.setIsSpecMenuOpen((open) => !open);
|
||||
setActiveTool('spec');
|
||||
return true;
|
||||
}
|
||||
if (tool === 'character') {
|
||||
generationWorkflow.openCharacterGenerationDialog();
|
||||
return true;
|
||||
}
|
||||
if (tool === 'icon') {
|
||||
generationWorkflow.openIconGenerationDialog();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[generationWorkflow, setActiveTool],
|
||||
);
|
||||
|
||||
const generationComposerNode = (
|
||||
<ImageCanvasGenerationComposerView
|
||||
specToolWrapRef={specToolWrapRef}
|
||||
characterSpecButtonRef={characterSpecButtonRef}
|
||||
characterReferenceButtonRef={characterReferenceButtonRef}
|
||||
iconSpecButtonRef={iconSpecButtonRef}
|
||||
isSpecMenuOpen={generationWorkflow.isSpecMenuOpen}
|
||||
isCharacterSpecMenuOpen={generationWorkflow.isCharacterSpecMenuOpen}
|
||||
isCharacterReferenceMenuOpen={
|
||||
generationWorkflow.isCharacterReferenceMenuOpen
|
||||
}
|
||||
isIconSpecMenuOpen={generationWorkflow.isIconSpecMenuOpen}
|
||||
isPickingCharacterSpecFromCanvas={
|
||||
generationWorkflow.isPickingCharacterSpecFromCanvas
|
||||
}
|
||||
isPickingCharacterReferenceFromCanvas={
|
||||
generationWorkflow.isPickingCharacterReferenceFromCanvas
|
||||
}
|
||||
isPickingIconSpecFromCanvas={
|
||||
generationWorkflow.isPickingIconSpecFromCanvas
|
||||
}
|
||||
generateDialog={generateDialog}
|
||||
generationComposerStyle={generationComposerStyle}
|
||||
iconComposerStyle={iconComposerStyle}
|
||||
quickEditPanel={generationWorkflow.quickEditPanel}
|
||||
quickEditSourceLayer={generationWorkflow.quickEditSourceLayer}
|
||||
quickEditPanelStyle={quickEditPanelStyle}
|
||||
quickEditSizeOptions={generationWorkflow.quickEditSizeOptions}
|
||||
quickEditModelOptions={generationWorkflow.quickEditModelOptions}
|
||||
characterAnimationPanel={generationWorkflow.characterAnimationPanel}
|
||||
characterAnimationSourceLayer={
|
||||
generationWorkflow.characterAnimationSourceLayer
|
||||
}
|
||||
characterAnimationPanelStyle={characterAnimationPanelStyle}
|
||||
characterAnimationPrice={generationWorkflow.characterAnimationPrice}
|
||||
setGenerateDialog={setGenerateDialog}
|
||||
setQuickEditPanel={generationWorkflow.setQuickEditPanel}
|
||||
setCharacterAnimationPanel={
|
||||
generationWorkflow.setCharacterAnimationPanel
|
||||
}
|
||||
setIsCharacterSpecMenuOpen={
|
||||
generationWorkflow.setIsCharacterSpecMenuOpen
|
||||
}
|
||||
setIsCharacterReferenceMenuOpen={
|
||||
generationWorkflow.setIsCharacterReferenceMenuOpen
|
||||
}
|
||||
setIsIconSpecMenuOpen={generationWorkflow.setIsIconSpecMenuOpen}
|
||||
setIsPickingCharacterSpecFromCanvas={
|
||||
generationWorkflow.setIsPickingCharacterSpecFromCanvas
|
||||
}
|
||||
setIsPickingCharacterReferenceFromCanvas={
|
||||
generationWorkflow.setIsPickingCharacterReferenceFromCanvas
|
||||
}
|
||||
setIsPickingIconSpecFromCanvas={
|
||||
generationWorkflow.setIsPickingIconSpecFromCanvas
|
||||
}
|
||||
onOpenSpecDialog={generationWorkflow.openSpecDialog}
|
||||
onRequestUpload={requestUpload}
|
||||
onSubmitImageGeneration={(dialog) =>
|
||||
void generationWorkflow.submitImageGeneration(dialog)
|
||||
}
|
||||
onSubmitIconSpritesheetGeneration={(dialog) =>
|
||||
void generationWorkflow.submitIconSpritesheetGeneration(dialog)
|
||||
}
|
||||
onSubmitQuickEdit={() => void generationWorkflow.submitQuickEdit()}
|
||||
onSubmitCharacterAnimation={() =>
|
||||
void generationWorkflow.submitCharacterAnimation()
|
||||
}
|
||||
onCloseGenerateComposer={generationWorkflow.closeGenerateComposer}
|
||||
onUpdateSpecFormValue={generationWorkflow.updateSpecFormValue}
|
||||
onUpdateIconDescription={generationWorkflow.updateIconDescription}
|
||||
onAddIconDescription={generationWorkflow.addIconDescription}
|
||||
onUpdateCharacterAnimationDuration={
|
||||
generationWorkflow.updateCharacterAnimationDuration
|
||||
}
|
||||
onRememberImageModel={generationWorkflow.rememberImageModel}
|
||||
/>
|
||||
);
|
||||
|
||||
return {
|
||||
...generationWorkflow,
|
||||
generationComposerStyle,
|
||||
generationComposerNode,
|
||||
switchGenerationTool,
|
||||
};
|
||||
}
|
||||
@@ -53,7 +53,6 @@ function StageControllerHarness({
|
||||
layers,
|
||||
selectedLayerId,
|
||||
selectedLayerIds,
|
||||
activeCanvasGenerationDialog: null,
|
||||
imageContextMenu,
|
||||
setImageContextMenu,
|
||||
contextMenu,
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
|
||||
import type {
|
||||
CanvasContextMenuState,
|
||||
CanvasGenerationDialogState,
|
||||
CanvasLayer,
|
||||
CanvasViewport,
|
||||
ImageContextMenuState,
|
||||
@@ -23,7 +22,6 @@ type UseImageCanvasStageControllerOptions = {
|
||||
layers: CanvasLayer[];
|
||||
selectedLayerId: string | null;
|
||||
selectedLayerIds: string[];
|
||||
activeCanvasGenerationDialog: CanvasGenerationDialogState | null;
|
||||
imageContextMenu: ImageContextMenuState | null;
|
||||
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
|
||||
contextMenu: CanvasContextMenuState | null;
|
||||
@@ -42,7 +40,6 @@ export function useImageCanvasStageController({
|
||||
layers,
|
||||
selectedLayerId,
|
||||
selectedLayerIds,
|
||||
activeCanvasGenerationDialog,
|
||||
imageContextMenu,
|
||||
setImageContextMenu,
|
||||
contextMenu,
|
||||
@@ -59,14 +56,12 @@ export function useImageCanvasStageController({
|
||||
layers,
|
||||
selectedLayerId,
|
||||
selectedLayerIds,
|
||||
activeCanvasGenerationDialog,
|
||||
imageContextMenu,
|
||||
contextMenu,
|
||||
viewport,
|
||||
canvasSize,
|
||||
}),
|
||||
[
|
||||
activeCanvasGenerationDialog,
|
||||
canvasSize,
|
||||
contextMenu,
|
||||
imageContextMenu,
|
||||
|
||||
@@ -4414,7 +4414,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
position: absolute;
|
||||
left: 0.85rem;
|
||||
bottom: 0.85rem;
|
||||
z-index: 11;
|
||||
z-index: 18;
|
||||
display: inline-flex;
|
||||
gap: 0.35rem;
|
||||
border: 1px solid #d9dee8;
|
||||
@@ -4702,7 +4702,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0.85rem;
|
||||
z-index: 10;
|
||||
z-index: 18;
|
||||
display: inline-flex;
|
||||
gap: 0.35rem;
|
||||
max-width: min(calc(100% - 6.6rem), 34rem);
|
||||
|
||||
Reference in New Issue
Block a user