抽出编辑器生成对话状态模型
新增 ImageCanvasGenerationDialogModel 承载生成面板草稿和引用选择规则 补充生成对话状态模型单测 精简 useImageCanvasGenerationWorkflow 中的面板状态构造 更新 TRACKING.md 记录第四十一阶段验证
This commit is contained in:
@@ -157,3 +157,4 @@
|
|||||||
- 2026-06-17 前端拆分第三十八阶段:继续收口 `useImageCanvasGenerationWorkflow`,扩展 `ImageCanvasGenerationSubmissionModel`,把图标素材批量生成的规范校验 / 描述清洗 / 请求 payload / generationInputs,以及角色动画生成的 prompt 清洗 / objectKey 优先源图 / 尺寸 / 价格 / 模型参数从 workflow hook 中抽成纯模型;workflow hook 继续保留对话状态、真实 API 调用、生成结果落图、失败恢复和角色动画面板生命周期。新增模型单测覆盖图标缺少规范、图标空描述、图标描述 trim / 参考快照,以及角色动画 trim、objectKey 源图和价格计算;`useImageCanvasGenerationWorkflow` 从 1104 行降至 1075 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.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` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;点击 `生成图标素材` 后 `Icon Generator` 占位和 `生成图标素材` 面板可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
- 2026-06-17 前端拆分第三十八阶段:继续收口 `useImageCanvasGenerationWorkflow`,扩展 `ImageCanvasGenerationSubmissionModel`,把图标素材批量生成的规范校验 / 描述清洗 / 请求 payload / generationInputs,以及角色动画生成的 prompt 清洗 / objectKey 优先源图 / 尺寸 / 价格 / 模型参数从 workflow hook 中抽成纯模型;workflow hook 继续保留对话状态、真实 API 调用、生成结果落图、失败恢复和角色动画面板生命周期。新增模型单测覆盖图标缺少规范、图标空描述、图标描述 trim / 参考快照,以及角色动画 trim、objectKey 源图和价格计算;`useImageCanvasGenerationWorkflow` 从 1104 行降至 1075 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.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` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见;点击 `生成图标素材` 后 `Icon Generator` 占位和 `生成图标素材` 面板可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
||||||
- 2026-06-17 前端拆分第三十九阶段:开始拆分 `ImageCanvasEditorView.test.tsx` 巨型集成测试,新增 `ImageCanvasEditorView.test-utils` 共享默认工程 / 素材库 mock、认证上下文、pointer / dataTransfer / deferred helper 和生命周期 setup;新增 `ImageCanvasEditorAssetsIntegration.test.tsx`,把素材库默认文件夹去重、文件夹折叠 / 新建 / 重命名 / 删除、素材重命名 / 拖拽移动、多文件上传、未登录续传、上传占位、401 登录、素材选择模式、删除素材同步清理画布图层、素材框选、画布 drop 上传和素材库拖入画布等集成用例从主测试文件迁出;`ImageCanvasEditorView.test.tsx` 从 4817 行降至 3741 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.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` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
- 2026-06-17 前端拆分第三十九阶段:开始拆分 `ImageCanvasEditorView.test.tsx` 巨型集成测试,新增 `ImageCanvasEditorView.test-utils` 共享默认工程 / 素材库 mock、认证上下文、pointer / dataTransfer / deferred helper 和生命周期 setup;新增 `ImageCanvasEditorAssetsIntegration.test.tsx`,把素材库默认文件夹去重、文件夹折叠 / 新建 / 重命名 / 删除、素材重命名 / 拖拽移动、多文件上传、未登录续传、上传占位、401 登录、素材选择模式、删除素材同步清理画布图层、素材框选、画布 drop 上传和素材库拖入画布等集成用例从主测试文件迁出;`ImageCanvasEditorView.test.tsx` 从 4817 行降至 3741 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.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` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
||||||
- 2026-06-17 前端拆分第四十阶段:继续拆分 `ImageCanvasEditorView.test.tsx` 巨型集成测试,新增 `ImageCanvasEditorGenerationIntegration.test.tsx`,把画布生图占位 / 生成提交、生成错误与未登录、规范生成、角色形象生成、图标素材生成、角色动画、快速编辑、生成图元数据和修改结果等生成相关集成用例从主测试文件迁出;`ImageCanvasEditorView.test.tsx` 从 3741 行降至 1419 行,主测试保留工程加载、导出、画布基础交互、右键菜单、侧栏、缩放、小地图、工具切换、吸附和历史。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.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` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
- 2026-06-17 前端拆分第四十阶段:继续拆分 `ImageCanvasEditorView.test.tsx` 巨型集成测试,新增 `ImageCanvasEditorGenerationIntegration.test.tsx`,把画布生图占位 / 生成提交、生成错误与未登录、规范生成、角色形象生成、图标素材生成、角色动画、快速编辑、生成图元数据和修改结果等生成相关集成用例从主测试文件迁出;`ImageCanvasEditorView.test.tsx` 从 3741 行降至 1419 行,主测试保留工程加载、导出、画布基础交互、右键菜单、侧栏、缩放、小地图、工具切换、吸附和历史。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasEditorView.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.test.tsx`、`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.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` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
||||||
|
- 2026-06-17 前端拆分第四十一阶段:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationDialogModel`,把普通生图 / 规范 / 角色形象 / 图标素材生成对话框草稿、修改图片 / 快速编辑 / 角色动画面板草稿、角色 / 图标参考选择、规范表单更新、图标描述更新、角色动画时长更新以及生成器失焦 / 关闭规则从 hook 中抽成纯模型;workflow hook 保留真实 API 调用、生成结果落画布、侧栏 / 工具 / 选中态副作用和错误回写。新增模型单测覆盖各类草稿、失败态清理、引用选择、描述限制、动画时长和 composer 可见性;`useImageCanvasGenerationWorkflow.ts` 从 1075 行降至 870 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasAssetRowView.test.tsx src/components/image-editor/ImageCanvasSidebarView.test.tsx src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasCharacterGenerationComposerView.test.tsx src/components/image-editor/ImageCanvasEditGenerationModalView.test.tsx src/components/image-editor/ImageCanvasEditorShellView.test.tsx src/components/image-editor/ImageCanvasBottomToolbarView.test.tsx src/components/image-editor/ImageCanvasPanelDockView.test.tsx src/components/image-editor/ImageCanvasContextMenusView.test.tsx src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx src/components/image-editor/ImageCanvasIconSpritesheetComposerView.test.tsx src/components/image-editor/ImageCanvasSpecGenerationPanelView.test.tsx src/components/image-editor/ImageCanvasGenerationImageOptionsView.test.tsx src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/ImageCanvasCharacterAnimationPanelView.test.tsx src/components/image-editor/ImageCanvasWorldView.test.tsx src/components/image-editor/useImageCanvasAssetLibrary.test.tsx src/components/image-editor/useImageCanvasGenerationSurface.test.tsx src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx src/components/image-editor/ImageCanvasEditorGenerationIntegration.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` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏标题为 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
CanvasLayer,
|
||||||
|
CharacterAnimationPanelState,
|
||||||
|
GenerateDialogState,
|
||||||
|
} from './ImageCanvasEditorTypes';
|
||||||
|
import {
|
||||||
|
appendCharacterReference,
|
||||||
|
appendIconDescriptionToDialog,
|
||||||
|
assignCharacterSpecReference,
|
||||||
|
assignIconSpecReference,
|
||||||
|
closeGenerateComposerDialog,
|
||||||
|
createCharacterAnimationPanelDraft,
|
||||||
|
createCharacterGenerationDialogDraft,
|
||||||
|
createEditDialogDraft,
|
||||||
|
createGenerateDialogDraft,
|
||||||
|
createIconGenerationDialogDraft,
|
||||||
|
createQuickEditPanelDraft,
|
||||||
|
createSpecDialogDraft,
|
||||||
|
hideGeneratedLayerComposerAfterBlur,
|
||||||
|
updateCharacterAnimationDurationPanel,
|
||||||
|
updateIconDescriptionInDialog,
|
||||||
|
updateSpecFormDialogValue,
|
||||||
|
} from './ImageCanvasGenerationDialogModel';
|
||||||
|
import { ICON_DESCRIPTION_LIMIT } from './ImageCanvasGenerationModel';
|
||||||
|
|
||||||
|
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
|
||||||
|
return {
|
||||||
|
id: 'layer-source',
|
||||||
|
resourceId: 'resource-source',
|
||||||
|
title: '源图',
|
||||||
|
src: 'data:image/png;base64,source',
|
||||||
|
x: 120,
|
||||||
|
y: 140,
|
||||||
|
width: 320,
|
||||||
|
height: 240,
|
||||||
|
originalWidth: 1024,
|
||||||
|
originalHeight: 768,
|
||||||
|
zIndex: 2,
|
||||||
|
sourceType: 'uploaded',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ImageCanvasGenerationDialogModel', () => {
|
||||||
|
it('creates centered drafts for image and spec generation dialogs', () => {
|
||||||
|
const canvasSize = { width: 1000, height: 800 };
|
||||||
|
const viewport = { x: 100, y: 40, scale: 2 };
|
||||||
|
|
||||||
|
expect(createGenerateDialogDraft({ canvasSize, viewport })).toMatchObject({
|
||||||
|
mode: 'generate',
|
||||||
|
status: 'idle',
|
||||||
|
composerOpen: true,
|
||||||
|
placeholder: {
|
||||||
|
x: -10,
|
||||||
|
y: -30,
|
||||||
|
width: 420,
|
||||||
|
height: 420,
|
||||||
|
originalWidth: 2048,
|
||||||
|
originalHeight: 2048,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
createSpecDialogDraft({ canvasSize, viewport, specType: 'icon' }),
|
||||||
|
).toMatchObject({
|
||||||
|
mode: 'spec',
|
||||||
|
specType: 'icon',
|
||||||
|
specValues: {
|
||||||
|
playSetting: '休闲小游戏',
|
||||||
|
artStyle: '清爽卡通',
|
||||||
|
},
|
||||||
|
placeholder: {
|
||||||
|
x: -80,
|
||||||
|
y: 22.5,
|
||||||
|
width: 560,
|
||||||
|
height: 315,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates character and icon generation drafts with model dimensions', () => {
|
||||||
|
const canvasSize = { width: 960, height: 720 };
|
||||||
|
const viewport = { x: 0, y: 0, scale: 1 };
|
||||||
|
|
||||||
|
expect(
|
||||||
|
createCharacterGenerationDialogDraft({
|
||||||
|
canvasSize,
|
||||||
|
viewport,
|
||||||
|
imageModel: 'gpt-image-2',
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
mode: 'character',
|
||||||
|
imageModel: 'gpt-image-2',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
imageSize: '1K',
|
||||||
|
characterSpecReference: null,
|
||||||
|
characterReferences: [],
|
||||||
|
placeholder: {
|
||||||
|
x: 270,
|
||||||
|
y: 150,
|
||||||
|
width: 420,
|
||||||
|
height: 420,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
createIconGenerationDialogDraft({
|
||||||
|
canvasSize,
|
||||||
|
viewport,
|
||||||
|
imageModel: 'unknown-model',
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
mode: 'icon',
|
||||||
|
imageModel: 'unknown-model',
|
||||||
|
aspectRatio: '1:1',
|
||||||
|
imageSize: '1K',
|
||||||
|
iconSpecReference: null,
|
||||||
|
iconDescriptions: [
|
||||||
|
'返回按钮',
|
||||||
|
'设置按钮',
|
||||||
|
'下一关按钮',
|
||||||
|
'提示按钮',
|
||||||
|
'原图按钮',
|
||||||
|
'冻结按钮',
|
||||||
|
],
|
||||||
|
placeholder: {
|
||||||
|
x: 300,
|
||||||
|
y: 180,
|
||||||
|
width: 360,
|
||||||
|
height: 360,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates edit, quick-edit, and character animation panel drafts', () => {
|
||||||
|
const sourceLayer = createLayer({
|
||||||
|
prompt: '原图提示',
|
||||||
|
model: 'gpt-image-2',
|
||||||
|
assetKind: 'character',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(createEditDialogDraft(sourceLayer)).toEqual({
|
||||||
|
mode: 'edit',
|
||||||
|
prompt: '原图提示,在保持主体结构的基础上优化画面细节',
|
||||||
|
status: 'idle',
|
||||||
|
composerOpen: true,
|
||||||
|
sourceLayerId: 'layer-source',
|
||||||
|
});
|
||||||
|
expect(createQuickEditPanelDraft(sourceLayer)).toEqual({
|
||||||
|
sourceLayerId: 'layer-source',
|
||||||
|
prompt: '',
|
||||||
|
size: '1024x768',
|
||||||
|
model: 'gpt-image-2',
|
||||||
|
status: 'idle',
|
||||||
|
});
|
||||||
|
expect(createCharacterAnimationPanelDraft(sourceLayer)).toEqual({
|
||||||
|
sourceLayerId: 'layer-source',
|
||||||
|
promptText: '',
|
||||||
|
resolution: '480p',
|
||||||
|
ratio: 'same',
|
||||||
|
frameCount: 32,
|
||||||
|
durationSeconds: 4,
|
||||||
|
status: 'idle',
|
||||||
|
});
|
||||||
|
expect(createCharacterAnimationPanelDraft(createLayer())).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets failed character and icon dialogs when picking references', () => {
|
||||||
|
const characterDialog: GenerateDialogState = {
|
||||||
|
mode: 'character',
|
||||||
|
prompt: '',
|
||||||
|
status: 'failed',
|
||||||
|
errorMessage: '请选择角色规范',
|
||||||
|
characterReferences: [],
|
||||||
|
};
|
||||||
|
const sourceLayer = createLayer({ title: '参考图' });
|
||||||
|
|
||||||
|
expect(assignCharacterSpecReference(characterDialog, sourceLayer)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
status: 'idle',
|
||||||
|
errorMessage: undefined,
|
||||||
|
composerOpen: true,
|
||||||
|
characterSpecReference: {
|
||||||
|
id: 'canvas-layer-source',
|
||||||
|
label: '参考图',
|
||||||
|
src: 'data:image/png;base64,source',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
appendCharacterReference(characterDialog, sourceLayer),
|
||||||
|
).toMatchObject({
|
||||||
|
status: 'idle',
|
||||||
|
errorMessage: undefined,
|
||||||
|
characterReferences: [{ label: '参考图' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const iconDialog: GenerateDialogState = {
|
||||||
|
mode: 'icon',
|
||||||
|
prompt: '',
|
||||||
|
status: 'failed',
|
||||||
|
errorMessage: '请选择图标素材规范',
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
assignIconSpecReference(
|
||||||
|
iconDialog,
|
||||||
|
createLayer({ assetKind: 'icon-spec' }),
|
||||||
|
),
|
||||||
|
).toMatchObject({
|
||||||
|
status: 'idle',
|
||||||
|
errorMessage: undefined,
|
||||||
|
iconSpecReference: { id: 'canvas-layer-source' },
|
||||||
|
});
|
||||||
|
expect(assignIconSpecReference(iconDialog, sourceLayer)).toBe(iconDialog);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates failed spec and icon dialog fields back to idle state', () => {
|
||||||
|
const specDialog: GenerateDialogState = {
|
||||||
|
mode: 'spec',
|
||||||
|
prompt: '',
|
||||||
|
status: 'failed',
|
||||||
|
errorMessage: '生成失败',
|
||||||
|
specType: 'custom',
|
||||||
|
specValues: {
|
||||||
|
playSetting: '',
|
||||||
|
artStyle: '',
|
||||||
|
bodyRatio: '3',
|
||||||
|
characterView: '',
|
||||||
|
customPrompt: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
updateSpecFormDialogValue(specDialog, 'customPrompt', '新规范'),
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
status: 'idle',
|
||||||
|
errorMessage: undefined,
|
||||||
|
specValues: expect.objectContaining({
|
||||||
|
customPrompt: '新规范',
|
||||||
|
bodyRatio: '3',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const iconDialog: GenerateDialogState = {
|
||||||
|
mode: 'icon',
|
||||||
|
prompt: '',
|
||||||
|
status: 'failed',
|
||||||
|
errorMessage: '描述错误',
|
||||||
|
iconDescriptions: ['旧描述'],
|
||||||
|
};
|
||||||
|
expect(updateIconDescriptionInDialog(iconDialog, 0, '新描述')).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
status: 'idle',
|
||||||
|
errorMessage: undefined,
|
||||||
|
iconDescriptions: ['新描述'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(appendIconDescriptionToDialog(iconDialog)).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
iconDescriptions: ['旧描述', ''],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
appendIconDescriptionToDialog({
|
||||||
|
...iconDialog,
|
||||||
|
iconDescriptions: Array.from(
|
||||||
|
{ length: ICON_DESCRIPTION_LIMIT },
|
||||||
|
(_, index) => `图标${index}`,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
iconDescriptions: expect.arrayContaining(['图标0']),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('updates character animation duration and composer visibility', () => {
|
||||||
|
const failedPanel: CharacterAnimationPanelState = {
|
||||||
|
sourceLayerId: 'layer-character',
|
||||||
|
promptText: '',
|
||||||
|
resolution: '480p',
|
||||||
|
ratio: 'same',
|
||||||
|
frameCount: 32,
|
||||||
|
durationSeconds: 4,
|
||||||
|
status: 'failed',
|
||||||
|
errorMessage: '失败',
|
||||||
|
};
|
||||||
|
expect(updateCharacterAnimationDurationPanel(failedPanel, '48')).toEqual({
|
||||||
|
...failedPanel,
|
||||||
|
frameCount: 48,
|
||||||
|
durationSeconds: 6,
|
||||||
|
status: 'idle',
|
||||||
|
errorMessage: undefined,
|
||||||
|
});
|
||||||
|
expect(updateCharacterAnimationDurationPanel(failedPanel, '999')).toBe(
|
||||||
|
failedPanel,
|
||||||
|
);
|
||||||
|
|
||||||
|
const generateDialog: GenerateDialogState = {
|
||||||
|
mode: 'generate',
|
||||||
|
prompt: '',
|
||||||
|
status: 'idle',
|
||||||
|
composerOpen: true,
|
||||||
|
};
|
||||||
|
expect(hideGeneratedLayerComposerAfterBlur(generateDialog)).toEqual({
|
||||||
|
...generateDialog,
|
||||||
|
composerOpen: false,
|
||||||
|
});
|
||||||
|
expect(closeGenerateComposerDialog(generateDialog)).toEqual({
|
||||||
|
...generateDialog,
|
||||||
|
composerOpen: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
369
src/components/image-editor/ImageCanvasGenerationDialogModel.ts
Normal file
369
src/components/image-editor/ImageCanvasGenerationDialogModel.ts
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
import { formatImageSizeValue } from './ImageCanvasEditorModel';
|
||||||
|
import type {
|
||||||
|
CanvasGenerationDialogState,
|
||||||
|
CanvasLayer,
|
||||||
|
CanvasViewport,
|
||||||
|
CharacterAnimationPanelState,
|
||||||
|
GenerateDialogState,
|
||||||
|
QuickEditPanelState,
|
||||||
|
SpecFormValues,
|
||||||
|
SpecGenerationType,
|
||||||
|
} from './ImageCanvasEditorTypes';
|
||||||
|
import {
|
||||||
|
CHARACTER_ANIMATION_DURATION_OPTIONS,
|
||||||
|
CHARACTER_FRAME_DISPLAY_SIZE,
|
||||||
|
CHARACTER_FRAME_ORIGINAL_SIZE,
|
||||||
|
createCanvasLayerReference,
|
||||||
|
DEFAULT_ICON_DESCRIPTIONS,
|
||||||
|
DEFAULT_IMAGE_MODEL,
|
||||||
|
DEFAULT_SPEC_FORM_VALUES,
|
||||||
|
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
||||||
|
ICON_DESCRIPTION_LIMIT,
|
||||||
|
ICON_FRAME_DISPLAY_SIZE,
|
||||||
|
ICON_FRAME_ORIGINAL_SIZE,
|
||||||
|
SPEC_FRAME_DISPLAY_SIZE,
|
||||||
|
SPEC_FRAME_ORIGINAL_SIZE,
|
||||||
|
} from './ImageCanvasGenerationModel';
|
||||||
|
|
||||||
|
type CanvasSize = { width: number; height: number };
|
||||||
|
|
||||||
|
function getViewportWorldCenter({
|
||||||
|
canvasSize,
|
||||||
|
viewport,
|
||||||
|
}: {
|
||||||
|
canvasSize: CanvasSize;
|
||||||
|
viewport: CanvasViewport;
|
||||||
|
}) {
|
||||||
|
const safeScale = viewport.scale > 0 ? viewport.scale : 1;
|
||||||
|
return {
|
||||||
|
x: (canvasSize.width / 2 - viewport.x) / safeScale,
|
||||||
|
y: (canvasSize.height / 2 - viewport.y) / safeScale,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFailedGenerationDialog(dialog: GenerateDialogState) {
|
||||||
|
return {
|
||||||
|
...dialog,
|
||||||
|
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
||||||
|
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveImageDimensionDefaults(imageModel: string) {
|
||||||
|
const dimensionOptions =
|
||||||
|
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
||||||
|
imageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
||||||
|
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
|
||||||
|
return {
|
||||||
|
aspectRatio: dimensionOptions.aspectRatios[0],
|
||||||
|
imageSize:
|
||||||
|
dimensionOptions.imageSizes.find((size) => size === '1K') ??
|
||||||
|
dimensionOptions.imageSizes[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createGenerateDialogDraft({
|
||||||
|
canvasSize,
|
||||||
|
viewport,
|
||||||
|
}: {
|
||||||
|
canvasSize: CanvasSize;
|
||||||
|
viewport: CanvasViewport;
|
||||||
|
}): Omit<CanvasGenerationDialogState, 'id'> {
|
||||||
|
const placeholderWidth = 420;
|
||||||
|
const placeholderHeight = 420;
|
||||||
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
||||||
|
return {
|
||||||
|
mode: 'generate',
|
||||||
|
prompt: '',
|
||||||
|
status: 'idle',
|
||||||
|
composerOpen: true,
|
||||||
|
placeholder: {
|
||||||
|
x: worldCenter.x - placeholderWidth / 2,
|
||||||
|
y: worldCenter.y - placeholderHeight / 2,
|
||||||
|
width: placeholderWidth,
|
||||||
|
height: placeholderHeight,
|
||||||
|
originalWidth: 2048,
|
||||||
|
originalHeight: 2048,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createSpecDialogDraft({
|
||||||
|
canvasSize,
|
||||||
|
viewport,
|
||||||
|
specType,
|
||||||
|
}: {
|
||||||
|
canvasSize: CanvasSize;
|
||||||
|
viewport: CanvasViewport;
|
||||||
|
specType: SpecGenerationType;
|
||||||
|
}): Omit<CanvasGenerationDialogState, 'id'> {
|
||||||
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
||||||
|
return {
|
||||||
|
mode: 'spec',
|
||||||
|
prompt: '',
|
||||||
|
status: 'idle',
|
||||||
|
composerOpen: true,
|
||||||
|
specType,
|
||||||
|
specValues: { ...DEFAULT_SPEC_FORM_VALUES[specType] },
|
||||||
|
placeholder: {
|
||||||
|
x: worldCenter.x - SPEC_FRAME_DISPLAY_SIZE.width / 2,
|
||||||
|
y: worldCenter.y - SPEC_FRAME_DISPLAY_SIZE.height / 2,
|
||||||
|
width: SPEC_FRAME_DISPLAY_SIZE.width,
|
||||||
|
height: SPEC_FRAME_DISPLAY_SIZE.height,
|
||||||
|
originalWidth: SPEC_FRAME_ORIGINAL_SIZE.width,
|
||||||
|
originalHeight: SPEC_FRAME_ORIGINAL_SIZE.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCharacterGenerationDialogDraft({
|
||||||
|
canvasSize,
|
||||||
|
viewport,
|
||||||
|
imageModel,
|
||||||
|
}: {
|
||||||
|
canvasSize: CanvasSize;
|
||||||
|
viewport: CanvasViewport;
|
||||||
|
imageModel: string;
|
||||||
|
}): Omit<CanvasGenerationDialogState, 'id'> {
|
||||||
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
||||||
|
const dimensionDefaults = resolveImageDimensionDefaults(imageModel);
|
||||||
|
return {
|
||||||
|
mode: 'character',
|
||||||
|
prompt: '',
|
||||||
|
status: 'idle',
|
||||||
|
composerOpen: true,
|
||||||
|
characterSpecReference: null,
|
||||||
|
characterReferences: [],
|
||||||
|
imageModel,
|
||||||
|
aspectRatio: dimensionDefaults.aspectRatio,
|
||||||
|
imageSize: dimensionDefaults.imageSize,
|
||||||
|
placeholder: {
|
||||||
|
x: worldCenter.x - CHARACTER_FRAME_DISPLAY_SIZE.width / 2,
|
||||||
|
y: worldCenter.y - CHARACTER_FRAME_DISPLAY_SIZE.height / 2,
|
||||||
|
width: CHARACTER_FRAME_DISPLAY_SIZE.width,
|
||||||
|
height: CHARACTER_FRAME_DISPLAY_SIZE.height,
|
||||||
|
originalWidth: CHARACTER_FRAME_ORIGINAL_SIZE.width,
|
||||||
|
originalHeight: CHARACTER_FRAME_ORIGINAL_SIZE.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createIconGenerationDialogDraft({
|
||||||
|
canvasSize,
|
||||||
|
viewport,
|
||||||
|
imageModel,
|
||||||
|
}: {
|
||||||
|
canvasSize: CanvasSize;
|
||||||
|
viewport: CanvasViewport;
|
||||||
|
imageModel: string;
|
||||||
|
}): Omit<CanvasGenerationDialogState, 'id'> {
|
||||||
|
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
||||||
|
const dimensionDefaults = resolveImageDimensionDefaults(imageModel);
|
||||||
|
return {
|
||||||
|
mode: 'icon',
|
||||||
|
prompt: '',
|
||||||
|
status: 'idle',
|
||||||
|
composerOpen: true,
|
||||||
|
iconSpecReference: null,
|
||||||
|
iconDescriptions: [...DEFAULT_ICON_DESCRIPTIONS],
|
||||||
|
imageModel,
|
||||||
|
aspectRatio: dimensionDefaults.aspectRatio,
|
||||||
|
imageSize: dimensionDefaults.imageSize,
|
||||||
|
placeholder: {
|
||||||
|
x: worldCenter.x - ICON_FRAME_DISPLAY_SIZE.width / 2,
|
||||||
|
y: worldCenter.y - ICON_FRAME_DISPLAY_SIZE.height / 2,
|
||||||
|
width: ICON_FRAME_DISPLAY_SIZE.width,
|
||||||
|
height: ICON_FRAME_DISPLAY_SIZE.height,
|
||||||
|
originalWidth: ICON_FRAME_ORIGINAL_SIZE.width,
|
||||||
|
originalHeight: ICON_FRAME_ORIGINAL_SIZE.height,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createEditDialogDraft(
|
||||||
|
sourceLayer: CanvasLayer,
|
||||||
|
): GenerateDialogState {
|
||||||
|
return {
|
||||||
|
mode: 'edit',
|
||||||
|
prompt: sourceLayer.prompt
|
||||||
|
? `${sourceLayer.prompt},在保持主体结构的基础上优化画面细节`
|
||||||
|
: '',
|
||||||
|
status: 'idle',
|
||||||
|
composerOpen: true,
|
||||||
|
sourceLayerId: sourceLayer.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createQuickEditPanelDraft(
|
||||||
|
sourceLayer: CanvasLayer,
|
||||||
|
): QuickEditPanelState {
|
||||||
|
return {
|
||||||
|
sourceLayerId: sourceLayer.id,
|
||||||
|
prompt: '',
|
||||||
|
size: formatImageSizeValue(
|
||||||
|
sourceLayer.originalWidth,
|
||||||
|
sourceLayer.originalHeight,
|
||||||
|
),
|
||||||
|
model: sourceLayer.model?.trim() || DEFAULT_IMAGE_MODEL,
|
||||||
|
status: 'idle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createCharacterAnimationPanelDraft(
|
||||||
|
layer: CanvasLayer,
|
||||||
|
): CharacterAnimationPanelState | null {
|
||||||
|
if (layer.assetKind !== 'character') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
sourceLayerId: layer.id,
|
||||||
|
promptText: '',
|
||||||
|
resolution: '480p',
|
||||||
|
ratio: 'same',
|
||||||
|
frameCount: 32,
|
||||||
|
durationSeconds: 4,
|
||||||
|
status: 'idle',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assignCharacterSpecReference(
|
||||||
|
dialog: GenerateDialogState | null,
|
||||||
|
layer: CanvasLayer,
|
||||||
|
): GenerateDialogState | null {
|
||||||
|
return dialog?.mode === 'character'
|
||||||
|
? {
|
||||||
|
...resetFailedGenerationDialog(dialog),
|
||||||
|
characterSpecReference: createCanvasLayerReference(layer),
|
||||||
|
composerOpen: true,
|
||||||
|
}
|
||||||
|
: dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appendCharacterReference(
|
||||||
|
dialog: GenerateDialogState | null,
|
||||||
|
layer: CanvasLayer,
|
||||||
|
): GenerateDialogState | null {
|
||||||
|
return dialog?.mode === 'character'
|
||||||
|
? {
|
||||||
|
...resetFailedGenerationDialog(dialog),
|
||||||
|
characterReferences: [
|
||||||
|
...(dialog.characterReferences ?? []),
|
||||||
|
createCanvasLayerReference(layer),
|
||||||
|
],
|
||||||
|
composerOpen: true,
|
||||||
|
}
|
||||||
|
: dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function assignIconSpecReference(
|
||||||
|
dialog: GenerateDialogState | null,
|
||||||
|
layer: CanvasLayer,
|
||||||
|
): GenerateDialogState | null {
|
||||||
|
if (layer.assetKind !== 'icon-spec') {
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
return dialog?.mode === 'icon'
|
||||||
|
? {
|
||||||
|
...resetFailedGenerationDialog(dialog),
|
||||||
|
iconSpecReference: createCanvasLayerReference(layer),
|
||||||
|
composerOpen: true,
|
||||||
|
}
|
||||||
|
: dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSpecFormDialogValue(
|
||||||
|
dialog: GenerateDialogState | null,
|
||||||
|
key: keyof SpecFormValues,
|
||||||
|
value: string,
|
||||||
|
): GenerateDialogState | null {
|
||||||
|
if (dialog?.mode !== 'spec') {
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
const specType = dialog.specType ?? 'custom';
|
||||||
|
return {
|
||||||
|
...resetFailedGenerationDialog(dialog),
|
||||||
|
specValues: {
|
||||||
|
...DEFAULT_SPEC_FORM_VALUES[specType],
|
||||||
|
...dialog.specValues,
|
||||||
|
[key]: value,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateIconDescriptionInDialog(
|
||||||
|
dialog: GenerateDialogState | null,
|
||||||
|
index: number,
|
||||||
|
value: string,
|
||||||
|
): GenerateDialogState | null {
|
||||||
|
return dialog?.mode === 'icon'
|
||||||
|
? {
|
||||||
|
...resetFailedGenerationDialog(dialog),
|
||||||
|
iconDescriptions: (
|
||||||
|
dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS
|
||||||
|
).map((description, descriptionIndex) =>
|
||||||
|
descriptionIndex === index ? value : description,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function appendIconDescriptionToDialog(
|
||||||
|
dialog: GenerateDialogState | null,
|
||||||
|
): GenerateDialogState | null {
|
||||||
|
if (dialog?.mode !== 'icon') {
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
const descriptions = dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS;
|
||||||
|
if (descriptions.length >= ICON_DESCRIPTION_LIMIT) {
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...resetFailedGenerationDialog(dialog),
|
||||||
|
iconDescriptions: [...descriptions, ''],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCharacterAnimationDurationPanel(
|
||||||
|
panel: CharacterAnimationPanelState | null,
|
||||||
|
frameCountValue: string,
|
||||||
|
): CharacterAnimationPanelState | null {
|
||||||
|
const option = CHARACTER_ANIMATION_DURATION_OPTIONS.find(
|
||||||
|
(item) => String(item.frameCount) === frameCountValue,
|
||||||
|
);
|
||||||
|
if (!option || !panel) {
|
||||||
|
return panel;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...panel,
|
||||||
|
frameCount: option.frameCount,
|
||||||
|
durationSeconds: option.durationSeconds,
|
||||||
|
status: panel.status === 'failed' ? 'idle' : panel.status,
|
||||||
|
errorMessage: panel.status === 'failed' ? undefined : panel.errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hideGeneratedLayerComposerAfterBlur(
|
||||||
|
dialog: GenerateDialogState | null,
|
||||||
|
): GenerateDialogState | null {
|
||||||
|
return (dialog?.mode === 'generate' ||
|
||||||
|
dialog?.mode === 'spec' ||
|
||||||
|
dialog?.mode === 'character' ||
|
||||||
|
dialog?.mode === 'icon') &&
|
||||||
|
dialog.status !== 'generating'
|
||||||
|
? {
|
||||||
|
...dialog,
|
||||||
|
composerOpen: false,
|
||||||
|
}
|
||||||
|
: dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeGenerateComposerDialog(
|
||||||
|
dialog: GenerateDialogState | null,
|
||||||
|
): GenerateDialogState | null {
|
||||||
|
return dialog?.mode === 'generate'
|
||||||
|
? {
|
||||||
|
...dialog,
|
||||||
|
composerOpen: false,
|
||||||
|
}
|
||||||
|
: dialog;
|
||||||
|
}
|
||||||
@@ -1,54 +1,22 @@
|
|||||||
import {
|
import {
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
type Dispatch,
|
type Dispatch,
|
||||||
type MutableRefObject,
|
type MutableRefObject,
|
||||||
type SetStateAction,
|
type SetStateAction,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
|
import { resolveEditorImageReferenceDataUrl } from '../../services/image-editor/editorImageReference';
|
||||||
import {
|
import {
|
||||||
editEditorImage,
|
editEditorImage,
|
||||||
generateEditorCharacterAnimation,
|
|
||||||
generateEditorIconSpritesheet,
|
|
||||||
generateEditorImage,
|
|
||||||
type EditorIconSpritesheetGenerationResult,
|
type EditorIconSpritesheetGenerationResult,
|
||||||
type EditorIconSpritesheetIconResult,
|
type EditorIconSpritesheetIconResult,
|
||||||
type EditorImageGenerationResult,
|
type EditorImageGenerationResult,
|
||||||
|
generateEditorCharacterAnimation,
|
||||||
|
generateEditorIconSpritesheet,
|
||||||
|
generateEditorImage,
|
||||||
} from '../../services/image-editor/editorProjectClient';
|
} from '../../services/image-editor/editorProjectClient';
|
||||||
import {
|
|
||||||
createGeneratedResultLayer,
|
|
||||||
createIconSpritesheetResultLayers,
|
|
||||||
createQuickEditResultLayer,
|
|
||||||
} from './ImageCanvasGenerationLayerModel';
|
|
||||||
import {
|
|
||||||
CHARACTER_ANIMATION_DURATION_OPTIONS,
|
|
||||||
CHARACTER_FRAME_DISPLAY_SIZE,
|
|
||||||
CHARACTER_FRAME_ORIGINAL_SIZE,
|
|
||||||
DEFAULT_ICON_DESCRIPTIONS,
|
|
||||||
DEFAULT_IMAGE_MODEL,
|
|
||||||
DEFAULT_SPEC_FORM_VALUES,
|
|
||||||
EDITOR_IMAGE_DIMENSION_OPTIONS,
|
|
||||||
ICON_DESCRIPTION_LIMIT,
|
|
||||||
ICON_FRAME_DISPLAY_SIZE,
|
|
||||||
ICON_FRAME_ORIGINAL_SIZE,
|
|
||||||
SPEC_FRAME_DISPLAY_SIZE,
|
|
||||||
SPEC_FRAME_ORIGINAL_SIZE,
|
|
||||||
buildEditGenerationInputs,
|
|
||||||
buildQuickEditModelOptions,
|
|
||||||
buildQuickEditSizeOptions,
|
|
||||||
calculateCharacterAnimationPrice,
|
|
||||||
createCanvasLayerReference,
|
|
||||||
isCanvasGenerationDialog,
|
|
||||||
resolveImageGenerationErrorMessage,
|
|
||||||
} from './ImageCanvasGenerationModel';
|
|
||||||
import {
|
|
||||||
buildCharacterAnimationSubmissionPlan,
|
|
||||||
buildIconSpritesheetGenerationSubmissionPlan,
|
|
||||||
buildImageGenerationSubmissionPlan,
|
|
||||||
} from './ImageCanvasGenerationSubmissionModel';
|
|
||||||
import { formatImageSizeValue } from './ImageCanvasEditorModel';
|
|
||||||
import type {
|
import type {
|
||||||
CanvasGenerationDialogState,
|
CanvasGenerationDialogState,
|
||||||
CanvasGenerationInputs,
|
CanvasGenerationInputs,
|
||||||
@@ -63,6 +31,44 @@ import type {
|
|||||||
SpecFormValues,
|
SpecFormValues,
|
||||||
SpecGenerationType,
|
SpecGenerationType,
|
||||||
} from './ImageCanvasEditorTypes';
|
} from './ImageCanvasEditorTypes';
|
||||||
|
import {
|
||||||
|
appendCharacterReference,
|
||||||
|
appendIconDescriptionToDialog,
|
||||||
|
assignCharacterSpecReference,
|
||||||
|
assignIconSpecReference,
|
||||||
|
closeGenerateComposerDialog,
|
||||||
|
createCharacterAnimationPanelDraft,
|
||||||
|
createCharacterGenerationDialogDraft,
|
||||||
|
createEditDialogDraft,
|
||||||
|
createGenerateDialogDraft,
|
||||||
|
createIconGenerationDialogDraft,
|
||||||
|
createQuickEditPanelDraft,
|
||||||
|
createSpecDialogDraft,
|
||||||
|
hideGeneratedLayerComposerAfterBlur,
|
||||||
|
updateCharacterAnimationDurationPanel,
|
||||||
|
updateIconDescriptionInDialog,
|
||||||
|
updateSpecFormDialogValue,
|
||||||
|
} from './ImageCanvasGenerationDialogModel';
|
||||||
|
import {
|
||||||
|
createGeneratedResultLayer,
|
||||||
|
createIconSpritesheetResultLayers,
|
||||||
|
createQuickEditResultLayer,
|
||||||
|
} from './ImageCanvasGenerationLayerModel';
|
||||||
|
import {
|
||||||
|
buildEditGenerationInputs,
|
||||||
|
buildQuickEditModelOptions,
|
||||||
|
buildQuickEditSizeOptions,
|
||||||
|
calculateCharacterAnimationPrice,
|
||||||
|
DEFAULT_ICON_DESCRIPTIONS,
|
||||||
|
DEFAULT_IMAGE_MODEL,
|
||||||
|
isCanvasGenerationDialog,
|
||||||
|
resolveImageGenerationErrorMessage,
|
||||||
|
} from './ImageCanvasGenerationModel';
|
||||||
|
import {
|
||||||
|
buildCharacterAnimationSubmissionPlan,
|
||||||
|
buildIconSpritesheetGenerationSubmissionPlan,
|
||||||
|
buildImageGenerationSubmissionPlan,
|
||||||
|
} from './ImageCanvasGenerationSubmissionModel';
|
||||||
|
|
||||||
type CanvasSize = { width: number; height: number };
|
type CanvasSize = { width: number; height: number };
|
||||||
|
|
||||||
@@ -98,36 +104,6 @@ type GenerationWorkflowOptions = {
|
|||||||
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
|
setImageContextMenu: Dispatch<SetStateAction<ImageContextMenuState | null>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getViewportWorldCenter({
|
|
||||||
canvasSize,
|
|
||||||
viewport,
|
|
||||||
}: {
|
|
||||||
canvasSize: CanvasSize;
|
|
||||||
viewport: CanvasViewport;
|
|
||||||
}) {
|
|
||||||
const safeScale = viewport.scale > 0 ? viewport.scale : 1;
|
|
||||||
return {
|
|
||||||
x: (canvasSize.width / 2 - viewport.x) / safeScale,
|
|
||||||
y: (canvasSize.height / 2 - viewport.y) / safeScale,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFailedCharacterGenerationIdle(dialog: GenerateDialogState) {
|
|
||||||
return {
|
|
||||||
...dialog,
|
|
||||||
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
||||||
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setFailedIconGenerationIdle(dialog: GenerateDialogState) {
|
|
||||||
return {
|
|
||||||
...dialog,
|
|
||||||
status: dialog.status === 'failed' ? 'idle' : dialog.status,
|
|
||||||
errorMessage: dialog.status === 'failed' ? undefined : dialog.errorMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useImageCanvasGenerationWorkflow({
|
export function useImageCanvasGenerationWorkflow({
|
||||||
layers,
|
layers,
|
||||||
canvasSize,
|
canvasSize,
|
||||||
@@ -149,8 +125,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
setImageContextMenu,
|
setImageContextMenu,
|
||||||
}: GenerationWorkflowOptions) {
|
}: GenerationWorkflowOptions) {
|
||||||
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
|
const [isSpecMenuOpen, setIsSpecMenuOpen] = useState(false);
|
||||||
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] =
|
const [isCharacterSpecMenuOpen, setIsCharacterSpecMenuOpen] = useState(false);
|
||||||
useState(false);
|
|
||||||
const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] =
|
const [isCharacterReferenceMenuOpen, setIsCharacterReferenceMenuOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [
|
const [
|
||||||
@@ -179,12 +154,16 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
(layer) => layer.id === characterAnimationPanel.sourceLayerId,
|
(layer) => layer.id === characterAnimationPanel.sourceLayerId,
|
||||||
) ?? null)
|
) ?? null)
|
||||||
: null;
|
: null;
|
||||||
const quickEditSizeOptions = quickEditPanel
|
const quickEditSizeOptions = useMemo(
|
||||||
? buildQuickEditSizeOptions(quickEditPanel.size)
|
() =>
|
||||||
: [];
|
quickEditPanel ? buildQuickEditSizeOptions(quickEditPanel.size) : [],
|
||||||
const quickEditModelOptions = quickEditPanel
|
[quickEditPanel],
|
||||||
? buildQuickEditModelOptions(quickEditPanel.model)
|
);
|
||||||
: [];
|
const quickEditModelOptions = useMemo(
|
||||||
|
() =>
|
||||||
|
quickEditPanel ? buildQuickEditModelOptions(quickEditPanel.model) : [],
|
||||||
|
[quickEditPanel],
|
||||||
|
);
|
||||||
const characterAnimationPrice = characterAnimationPanel
|
const characterAnimationPrice = characterAnimationPanel
|
||||||
? calculateCharacterAnimationPrice(
|
? calculateCharacterAnimationPrice(
|
||||||
characterAnimationPanel.resolution,
|
characterAnimationPanel.resolution,
|
||||||
@@ -197,23 +176,9 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
: DEFAULT_ICON_DESCRIPTIONS;
|
: DEFAULT_ICON_DESCRIPTIONS;
|
||||||
|
|
||||||
const openGenerateDialog = useCallback(() => {
|
const openGenerateDialog = useCallback(() => {
|
||||||
const placeholderWidth = 420;
|
openCanvasGenerationDialog(
|
||||||
const placeholderHeight = 420;
|
createGenerateDialogDraft({ canvasSize, viewport }),
|
||||||
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
);
|
||||||
openCanvasGenerationDialog({
|
|
||||||
mode: 'generate',
|
|
||||||
prompt: '',
|
|
||||||
status: 'idle',
|
|
||||||
composerOpen: true,
|
|
||||||
placeholder: {
|
|
||||||
x: worldCenter.x - placeholderWidth / 2,
|
|
||||||
y: worldCenter.y - placeholderHeight / 2,
|
|
||||||
width: placeholderWidth,
|
|
||||||
height: placeholderHeight,
|
|
||||||
originalWidth: 2048,
|
|
||||||
originalHeight: 2048,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setActiveTool('generate');
|
setActiveTool('generate');
|
||||||
selectSingleLayer(null);
|
selectSingleLayer(null);
|
||||||
setQuickEditPanel(null);
|
setQuickEditPanel(null);
|
||||||
@@ -227,23 +192,9 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
|
|
||||||
const openSpecDialog = useCallback(
|
const openSpecDialog = useCallback(
|
||||||
(specType: SpecGenerationType) => {
|
(specType: SpecGenerationType) => {
|
||||||
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
openCanvasGenerationDialog(
|
||||||
openCanvasGenerationDialog({
|
createSpecDialogDraft({ canvasSize, viewport, specType }),
|
||||||
mode: 'spec',
|
);
|
||||||
prompt: '',
|
|
||||||
status: 'idle',
|
|
||||||
composerOpen: true,
|
|
||||||
specType,
|
|
||||||
specValues: { ...DEFAULT_SPEC_FORM_VALUES[specType] },
|
|
||||||
placeholder: {
|
|
||||||
x: worldCenter.x - SPEC_FRAME_DISPLAY_SIZE.width / 2,
|
|
||||||
y: worldCenter.y - SPEC_FRAME_DISPLAY_SIZE.height / 2,
|
|
||||||
width: SPEC_FRAME_DISPLAY_SIZE.width,
|
|
||||||
height: SPEC_FRAME_DISPLAY_SIZE.height,
|
|
||||||
originalWidth: SPEC_FRAME_ORIGINAL_SIZE.width,
|
|
||||||
originalHeight: SPEC_FRAME_ORIGINAL_SIZE.height,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setIsSpecMenuOpen(false);
|
setIsSpecMenuOpen(false);
|
||||||
setActiveTool('generate');
|
setActiveTool('generate');
|
||||||
selectSingleLayer(null);
|
selectSingleLayer(null);
|
||||||
@@ -260,56 +211,30 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
|
|
||||||
const openCharacterAnimationPanel = useCallback(
|
const openCharacterAnimationPanel = useCallback(
|
||||||
(layer: CanvasLayer) => {
|
(layer: CanvasLayer) => {
|
||||||
if (layer.assetKind !== 'character') {
|
const nextPanel = createCharacterAnimationPanelDraft(layer);
|
||||||
|
if (!nextPanel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setImageContextMenu(null);
|
setImageContextMenu(null);
|
||||||
setQuickEditPanel(null);
|
setQuickEditPanel(null);
|
||||||
setCharacterAnimationPanel({
|
setCharacterAnimationPanel(nextPanel);
|
||||||
sourceLayerId: layer.id,
|
|
||||||
promptText: '',
|
|
||||||
resolution: '480p',
|
|
||||||
ratio: 'same',
|
|
||||||
frameCount: 32,
|
|
||||||
durationSeconds: 4,
|
|
||||||
status: 'idle',
|
|
||||||
});
|
|
||||||
selectSingleLayer(layer.id);
|
selectSingleLayer(layer.id);
|
||||||
},
|
},
|
||||||
[selectSingleLayer, setImageContextMenu],
|
[selectSingleLayer, setImageContextMenu],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openCharacterGenerationDialog = useCallback(() => {
|
const openCharacterGenerationDialog = useCallback(() => {
|
||||||
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
|
||||||
setIsSpecMenuOpen(false);
|
setIsSpecMenuOpen(false);
|
||||||
setIsCharacterReferenceMenuOpen(false);
|
setIsCharacterReferenceMenuOpen(false);
|
||||||
setIsPickingCharacterSpecFromCanvas(false);
|
setIsPickingCharacterSpecFromCanvas(false);
|
||||||
setIsPickingCharacterReferenceFromCanvas(false);
|
setIsPickingCharacterReferenceFromCanvas(false);
|
||||||
const dimensionOptions =
|
openCanvasGenerationDialog(
|
||||||
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
createCharacterGenerationDialogDraft({
|
||||||
lastImageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
canvasSize,
|
||||||
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
|
viewport,
|
||||||
openCanvasGenerationDialog({
|
imageModel: lastImageModel,
|
||||||
mode: 'character',
|
}),
|
||||||
prompt: '',
|
);
|
||||||
status: 'idle',
|
|
||||||
composerOpen: true,
|
|
||||||
characterSpecReference: null,
|
|
||||||
characterReferences: [],
|
|
||||||
imageModel: lastImageModel,
|
|
||||||
aspectRatio: dimensionOptions.aspectRatios[0],
|
|
||||||
imageSize:
|
|
||||||
dimensionOptions.imageSizes.find((size) => size === '1K') ??
|
|
||||||
dimensionOptions.imageSizes[0],
|
|
||||||
placeholder: {
|
|
||||||
x: worldCenter.x - CHARACTER_FRAME_DISPLAY_SIZE.width / 2,
|
|
||||||
y: worldCenter.y - CHARACTER_FRAME_DISPLAY_SIZE.height / 2,
|
|
||||||
width: CHARACTER_FRAME_DISPLAY_SIZE.width,
|
|
||||||
height: CHARACTER_FRAME_DISPLAY_SIZE.height,
|
|
||||||
originalWidth: CHARACTER_FRAME_ORIGINAL_SIZE.width,
|
|
||||||
originalHeight: CHARACTER_FRAME_ORIGINAL_SIZE.height,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setActiveTool('character');
|
setActiveTool('character');
|
||||||
selectSingleLayer(null);
|
selectSingleLayer(null);
|
||||||
setQuickEditPanel(null);
|
setQuickEditPanel(null);
|
||||||
@@ -323,37 +248,18 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const openIconGenerationDialog = useCallback(() => {
|
const openIconGenerationDialog = useCallback(() => {
|
||||||
const worldCenter = getViewportWorldCenter({ canvasSize, viewport });
|
|
||||||
setIsSpecMenuOpen(false);
|
setIsSpecMenuOpen(false);
|
||||||
setIsCharacterReferenceMenuOpen(false);
|
setIsCharacterReferenceMenuOpen(false);
|
||||||
setIsPickingCharacterSpecFromCanvas(false);
|
setIsPickingCharacterSpecFromCanvas(false);
|
||||||
setIsPickingCharacterReferenceFromCanvas(false);
|
setIsPickingCharacterReferenceFromCanvas(false);
|
||||||
setIsPickingIconSpecFromCanvas(false);
|
setIsPickingIconSpecFromCanvas(false);
|
||||||
const dimensionOptions =
|
openCanvasGenerationDialog(
|
||||||
EDITOR_IMAGE_DIMENSION_OPTIONS[
|
createIconGenerationDialogDraft({
|
||||||
lastImageModel as keyof typeof EDITOR_IMAGE_DIMENSION_OPTIONS
|
canvasSize,
|
||||||
] ?? EDITOR_IMAGE_DIMENSION_OPTIONS[DEFAULT_IMAGE_MODEL];
|
viewport,
|
||||||
openCanvasGenerationDialog({
|
imageModel: lastImageModel,
|
||||||
mode: 'icon',
|
}),
|
||||||
prompt: '',
|
);
|
||||||
status: 'idle',
|
|
||||||
composerOpen: true,
|
|
||||||
iconSpecReference: null,
|
|
||||||
iconDescriptions: [...DEFAULT_ICON_DESCRIPTIONS],
|
|
||||||
imageModel: lastImageModel,
|
|
||||||
aspectRatio: dimensionOptions.aspectRatios[0],
|
|
||||||
imageSize:
|
|
||||||
dimensionOptions.imageSizes.find((size) => size === '1K') ??
|
|
||||||
dimensionOptions.imageSizes[0],
|
|
||||||
placeholder: {
|
|
||||||
x: worldCenter.x - ICON_FRAME_DISPLAY_SIZE.width / 2,
|
|
||||||
y: worldCenter.y - ICON_FRAME_DISPLAY_SIZE.height / 2,
|
|
||||||
width: ICON_FRAME_DISPLAY_SIZE.width,
|
|
||||||
height: ICON_FRAME_DISPLAY_SIZE.height,
|
|
||||||
originalWidth: ICON_FRAME_ORIGINAL_SIZE.width,
|
|
||||||
originalHeight: ICON_FRAME_ORIGINAL_SIZE.height,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
setActiveTool('icon');
|
setActiveTool('icon');
|
||||||
selectSingleLayer(null);
|
selectSingleLayer(null);
|
||||||
setQuickEditPanel(null);
|
setQuickEditPanel(null);
|
||||||
@@ -372,15 +278,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
setMetadataLayer(null);
|
setMetadataLayer(null);
|
||||||
setImageContextMenu(null);
|
setImageContextMenu(null);
|
||||||
setQuickEditPanel(null);
|
setQuickEditPanel(null);
|
||||||
setGenerateDialog({
|
setGenerateDialog(createEditDialogDraft(sourceLayer));
|
||||||
mode: 'edit',
|
|
||||||
prompt: sourceLayer.prompt
|
|
||||||
? `${sourceLayer.prompt},在保持主体结构的基础上优化画面细节`
|
|
||||||
: '',
|
|
||||||
status: 'idle',
|
|
||||||
composerOpen: true,
|
|
||||||
sourceLayerId: sourceLayer.id,
|
|
||||||
});
|
|
||||||
setActiveTool('generate');
|
setActiveTool('generate');
|
||||||
},
|
},
|
||||||
[setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer],
|
[setActiveTool, setGenerateDialog, setImageContextMenu, setMetadataLayer],
|
||||||
@@ -392,16 +290,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
setMetadataLayer(null);
|
setMetadataLayer(null);
|
||||||
setGenerateDialog(null);
|
setGenerateDialog(null);
|
||||||
setCharacterAnimationPanel(null);
|
setCharacterAnimationPanel(null);
|
||||||
setQuickEditPanel({
|
setQuickEditPanel(createQuickEditPanelDraft(sourceLayer));
|
||||||
sourceLayerId: sourceLayer.id,
|
|
||||||
prompt: '',
|
|
||||||
size: formatImageSizeValue(
|
|
||||||
sourceLayer.originalWidth,
|
|
||||||
sourceLayer.originalHeight,
|
|
||||||
),
|
|
||||||
model: sourceLayer.model?.trim() || DEFAULT_IMAGE_MODEL,
|
|
||||||
status: 'idle',
|
|
||||||
});
|
|
||||||
selectSingleLayer(sourceLayer.id);
|
selectSingleLayer(sourceLayer.id);
|
||||||
setActiveTool('generate');
|
setActiveTool('generate');
|
||||||
},
|
},
|
||||||
@@ -556,13 +445,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
const pickCharacterSpecFromLayer = useCallback(
|
const pickCharacterSpecFromLayer = useCallback(
|
||||||
(layer: CanvasLayer) => {
|
(layer: CanvasLayer) => {
|
||||||
setGenerateDialog((currentDialog) =>
|
setGenerateDialog((currentDialog) =>
|
||||||
currentDialog?.mode === 'character'
|
assignCharacterSpecReference(currentDialog, layer),
|
||||||
? {
|
|
||||||
...setFailedCharacterGenerationIdle(currentDialog),
|
|
||||||
characterSpecReference: createCanvasLayerReference(layer),
|
|
||||||
composerOpen: true,
|
|
||||||
}
|
|
||||||
: currentDialog,
|
|
||||||
);
|
);
|
||||||
setIsPickingCharacterSpecFromCanvas(false);
|
setIsPickingCharacterSpecFromCanvas(false);
|
||||||
setIsCharacterSpecMenuOpen(false);
|
setIsCharacterSpecMenuOpen(false);
|
||||||
@@ -574,16 +457,7 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
const pickCharacterReferenceFromLayer = useCallback(
|
const pickCharacterReferenceFromLayer = useCallback(
|
||||||
(layer: CanvasLayer) => {
|
(layer: CanvasLayer) => {
|
||||||
setGenerateDialog((currentDialog) =>
|
setGenerateDialog((currentDialog) =>
|
||||||
currentDialog?.mode === 'character'
|
appendCharacterReference(currentDialog, layer),
|
||||||
? {
|
|
||||||
...setFailedCharacterGenerationIdle(currentDialog),
|
|
||||||
characterReferences: [
|
|
||||||
...(currentDialog.characterReferences ?? []),
|
|
||||||
createCanvasLayerReference(layer),
|
|
||||||
],
|
|
||||||
composerOpen: true,
|
|
||||||
}
|
|
||||||
: currentDialog,
|
|
||||||
);
|
);
|
||||||
setIsPickingCharacterReferenceFromCanvas(false);
|
setIsPickingCharacterReferenceFromCanvas(false);
|
||||||
setImageContextMenu(null);
|
setImageContextMenu(null);
|
||||||
@@ -593,18 +467,12 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
|
|
||||||
const pickIconSpecFromLayer = useCallback(
|
const pickIconSpecFromLayer = useCallback(
|
||||||
(layer: CanvasLayer) => {
|
(layer: CanvasLayer) => {
|
||||||
|
setGenerateDialog((currentDialog) =>
|
||||||
|
assignIconSpecReference(currentDialog, layer),
|
||||||
|
);
|
||||||
if (layer.assetKind !== 'icon-spec') {
|
if (layer.assetKind !== 'icon-spec') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setGenerateDialog((currentDialog) =>
|
|
||||||
currentDialog?.mode === 'icon'
|
|
||||||
? {
|
|
||||||
...setFailedIconGenerationIdle(currentDialog),
|
|
||||||
iconSpecReference: createCanvasLayerReference(layer),
|
|
||||||
composerOpen: true,
|
|
||||||
}
|
|
||||||
: currentDialog,
|
|
||||||
);
|
|
||||||
setIsPickingIconSpecFromCanvas(false);
|
setIsPickingIconSpecFromCanvas(false);
|
||||||
setIsIconSpecMenuOpen(false);
|
setIsIconSpecMenuOpen(false);
|
||||||
setImageContextMenu(null);
|
setImageContextMenu(null);
|
||||||
@@ -615,36 +483,14 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
const updateIconDescription = useCallback(
|
const updateIconDescription = useCallback(
|
||||||
(index: number, value: string) => {
|
(index: number, value: string) => {
|
||||||
setGenerateDialog((currentDialog) =>
|
setGenerateDialog((currentDialog) =>
|
||||||
currentDialog?.mode === 'icon'
|
updateIconDescriptionInDialog(currentDialog, index, value),
|
||||||
? {
|
|
||||||
...setFailedIconGenerationIdle(currentDialog),
|
|
||||||
iconDescriptions: (
|
|
||||||
currentDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS
|
|
||||||
).map((description, descriptionIndex) =>
|
|
||||||
descriptionIndex === index ? value : description,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
: currentDialog,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[setGenerateDialog],
|
[setGenerateDialog],
|
||||||
);
|
);
|
||||||
|
|
||||||
const addIconDescription = useCallback(() => {
|
const addIconDescription = useCallback(() => {
|
||||||
setGenerateDialog((currentDialog) => {
|
setGenerateDialog(appendIconDescriptionToDialog);
|
||||||
if (currentDialog?.mode !== 'icon') {
|
|
||||||
return currentDialog;
|
|
||||||
}
|
|
||||||
const descriptions =
|
|
||||||
currentDialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS;
|
|
||||||
if (descriptions.length >= ICON_DESCRIPTION_LIMIT) {
|
|
||||||
return currentDialog;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
...setFailedIconGenerationIdle(currentDialog),
|
|
||||||
iconDescriptions: [...descriptions, ''],
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}, [setGenerateDialog]);
|
}, [setGenerateDialog]);
|
||||||
|
|
||||||
const submitIconSpritesheetGeneration = useCallback(
|
const submitIconSpritesheetGeneration = useCallback(
|
||||||
@@ -841,52 +687,17 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
|
|
||||||
const updateSpecFormValue = useCallback(
|
const updateSpecFormValue = useCallback(
|
||||||
(key: keyof SpecFormValues, value: string) => {
|
(key: keyof SpecFormValues, value: string) => {
|
||||||
setGenerateDialog((currentDialog) => {
|
setGenerateDialog((currentDialog) =>
|
||||||
if (currentDialog?.mode !== 'spec') {
|
updateSpecFormDialogValue(currentDialog, key, value),
|
||||||
return currentDialog;
|
);
|
||||||
}
|
|
||||||
const specType = currentDialog.specType ?? 'custom';
|
|
||||||
return {
|
|
||||||
...currentDialog,
|
|
||||||
specValues: {
|
|
||||||
...DEFAULT_SPEC_FORM_VALUES[specType],
|
|
||||||
...currentDialog.specValues,
|
|
||||||
[key]: value,
|
|
||||||
},
|
|
||||||
status:
|
|
||||||
currentDialog.status === 'failed' ? 'idle' : currentDialog.status,
|
|
||||||
errorMessage:
|
|
||||||
currentDialog.status === 'failed'
|
|
||||||
? undefined
|
|
||||||
: currentDialog.errorMessage,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
[setGenerateDialog],
|
[setGenerateDialog],
|
||||||
);
|
);
|
||||||
|
|
||||||
const updateCharacterAnimationDuration = useCallback(
|
const updateCharacterAnimationDuration = useCallback(
|
||||||
(frameCountValue: string) => {
|
(frameCountValue: string) => {
|
||||||
const option = CHARACTER_ANIMATION_DURATION_OPTIONS.find(
|
|
||||||
(item) => String(item.frameCount) === frameCountValue,
|
|
||||||
);
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCharacterAnimationPanel((currentPanel) =>
|
setCharacterAnimationPanel((currentPanel) =>
|
||||||
currentPanel
|
updateCharacterAnimationDurationPanel(currentPanel, frameCountValue),
|
||||||
? {
|
|
||||||
...currentPanel,
|
|
||||||
frameCount: option.frameCount,
|
|
||||||
durationSeconds: option.durationSeconds,
|
|
||||||
status:
|
|
||||||
currentPanel.status === 'failed' ? 'idle' : currentPanel.status,
|
|
||||||
errorMessage:
|
|
||||||
currentPanel.status === 'failed'
|
|
||||||
? undefined
|
|
||||||
: currentPanel.errorMessage,
|
|
||||||
}
|
|
||||||
: currentPanel,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
@@ -940,28 +751,12 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
|
|
||||||
const hideGeneratedLayerPanelAfterBlur = useCallback(() => {
|
const hideGeneratedLayerPanelAfterBlur = useCallback(() => {
|
||||||
setGenerateDialog((currentDialog) =>
|
setGenerateDialog((currentDialog) =>
|
||||||
(currentDialog?.mode === 'generate' ||
|
hideGeneratedLayerComposerAfterBlur(currentDialog),
|
||||||
currentDialog?.mode === 'spec' ||
|
|
||||||
currentDialog?.mode === 'character' ||
|
|
||||||
currentDialog?.mode === 'icon') &&
|
|
||||||
currentDialog.status !== 'generating'
|
|
||||||
? {
|
|
||||||
...currentDialog,
|
|
||||||
composerOpen: false,
|
|
||||||
}
|
|
||||||
: currentDialog,
|
|
||||||
);
|
);
|
||||||
}, [setGenerateDialog]);
|
}, [setGenerateDialog]);
|
||||||
|
|
||||||
const closeGenerateComposer = useCallback(() => {
|
const closeGenerateComposer = useCallback(() => {
|
||||||
setGenerateDialog((currentDialog) =>
|
setGenerateDialog(closeGenerateComposerDialog);
|
||||||
currentDialog?.mode === 'generate'
|
|
||||||
? {
|
|
||||||
...currentDialog,
|
|
||||||
composerOpen: false,
|
|
||||||
}
|
|
||||||
: currentDialog,
|
|
||||||
);
|
|
||||||
setActiveTool('select');
|
setActiveTool('select');
|
||||||
}, [setActiveTool, setGenerateDialog]);
|
}, [setActiveTool, setGenerateDialog]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user