拆分编辑器高级生成提交模型

抽出图标素材生成校验和请求参数组装

抽出角色动画生成请求参数组装

补充高级生成提交模型单测

更新 TRACKING.md 记录第三十八阶段验证
This commit is contained in:
2026-06-17 18:27:33 +08:00
parent 6e8089c297
commit bf24d259a7
4 changed files with 242 additions and 54 deletions

View File

@@ -154,3 +154,4 @@
- 2026-06-17 前端拆分第三十五阶段:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationSubmissionModel`,把普通生图、修改图片、规范生成和角色形象生成的请求 payload、标准化 prompt、结果图层标题 / assetKind 和 generationInputs 快照构建从 workflow hook 中抽成纯模型workflow hook 保留对话状态、真实 API 调用、图片引用解析、结果落图、选中和 fit 副作用,避免拆散生成生命周期。新增模型单测覆盖普通生图、修改图、带参考图的规范生成、带规范 / 常规参考图的角色生成和缺失源图异常;`useImageCanvasGenerationWorkflow` 从 1167 行降至 1104 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx``npm run test -- 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/useImageCanvasGenerationSurface.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` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)``background-image: none`;点击 `生成工具``Image Generator``生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 - 2026-06-17 前端拆分第三十五阶段:继续收口 `useImageCanvasGenerationWorkflow`,新增 `ImageCanvasGenerationSubmissionModel`,把普通生图、修改图片、规范生成和角色形象生成的请求 payload、标准化 prompt、结果图层标题 / assetKind 和 generationInputs 快照构建从 workflow hook 中抽成纯模型workflow hook 保留对话状态、真实 API 调用、图片引用解析、结果落图、选中和 fit 副作用,避免拆散生成生命周期。新增模型单测覆盖普通生图、修改图、带参考图的规范生成、带规范 / 常规参考图的角色生成和缺失源图异常;`useImageCanvasGenerationWorkflow` 从 1167 行降至 1104 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx``npm run test -- 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/useImageCanvasGenerationSurface.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` 临时 `XDG_CONFIG_HOME` 下 Chrome headless 打开成功,未登录显示 `账号入口`;关闭后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)``background-image: none`;点击 `生成工具``Image Generator``生成图片` 对话框和 `AI画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
- 2026-06-17 前端拆分第三十六阶段:继续收口 `useImageCanvasUploadWorkflow`,新增 `ImageCanvasUploadModel`,把上传目标文件夹解析、上传中素材占位卡、上传到画布的临时图层、无效 drop 坐标兜底和图片真实尺寸回填坐标计算从 hook 中抽成纯模型upload workflow hook 保留登录恢复、文件读取、真实素材创建 API、上传进度状态和生成面板参考图写入副作用。新增模型单测覆盖文件夹兜底、占位素材、画布落点、非法坐标兜底和真实尺寸修正`useImageCanvasUploadWorkflow` 从 546 行降至 510 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx``npm run test -- 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/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画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 - 2026-06-17 前端拆分第三十六阶段:继续收口 `useImageCanvasUploadWorkflow`,新增 `ImageCanvasUploadModel`,把上传目标文件夹解析、上传中素材占位卡、上传到画布的临时图层、无效 drop 坐标兜底和图片真实尺寸回填坐标计算从 hook 中抽成纯模型upload workflow hook 保留登录恢复、文件读取、真实素材创建 API、上传进度状态和生成面板参考图写入副作用。新增模型单测覆盖文件夹兜底、占位素材、画布落点、非法坐标兜底和真实尺寸修正`useImageCanvasUploadWorkflow` 从 546 行降至 510 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasUploadModel.test.ts src/components/image-editor/useImageCanvasUploadWorkflow.test.tsx``npm run test -- 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/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画布工具栏` 均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
- 2026-06-17 前端拆分第三十七阶段:继续收口 `useImageCanvasAssetLibrary`,新增 `ImageCanvasAssetLibraryModel`,把素材分组、可选择素材筛选、全选状态、素材 / 文件夹重命名、文件夹折叠、本地新建文件夹占位、持久化文件夹替换、本地删除素材、删除文件夹回默认文件夹、选择集合切换、批量删除和本地移动素材到文件夹从 hook 中抽成纯模型asset library hook 继续保留加载账号素材库、后端 CRUD 调用、登录弹窗、DOM 框选和素材拖拽命中生命周期。新增模型单测覆盖分组 / 选择、重命名 / 折叠 / 本地文件夹、本地文件夹持久化替换、删除文件夹回默认文件夹、全选 / 批量删除和本地移动;`useImageCanvasAssetLibrary` 从 609 行降至 573 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/useImageCanvasAssetLibrary.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画布工具栏` 均可见;切回 `打开素材` 后侧栏显示 `素材``上传到项目素材` 入口可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。 - 2026-06-17 前端拆分第三十七阶段:继续收口 `useImageCanvasAssetLibrary`,新增 `ImageCanvasAssetLibraryModel`,把素材分组、可选择素材筛选、全选状态、素材 / 文件夹重命名、文件夹折叠、本地新建文件夹占位、持久化文件夹替换、本地删除素材、删除文件夹回默认文件夹、选择集合切换、批量删除和本地移动素材到文件夹从 hook 中抽成纯模型asset library hook 继续保留加载账号素材库、后端 CRUD 调用、登录弹窗、DOM 框选和素材拖拽命中生命周期。新增模型单测覆盖分组 / 选择、重命名 / 折叠 / 本地文件夹、本地文件夹持久化替换、删除文件夹回默认文件夹、全选 / 批量删除和本地移动;`useImageCanvasAssetLibrary` 从 609 行降至 573 行。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasAssetLibraryModel.test.ts src/components/image-editor/useImageCanvasAssetLibrary.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画布工具栏` 均可见;切回 `打开素材` 后侧栏显示 `素材``上传到项目素材` 入口可见,控制台仅有预期的未登录 `/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。

View File

@@ -1,7 +1,11 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import type { CanvasLayer } from './ImageCanvasEditorTypes'; import type { CanvasLayer } from './ImageCanvasEditorTypes';
import { buildImageGenerationSubmissionPlan } from './ImageCanvasGenerationSubmissionModel'; import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
buildImageGenerationSubmissionPlan,
} from './ImageCanvasGenerationSubmissionModel';
function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer { function createLayer(overrides: Partial<CanvasLayer> = {}): CanvasLayer {
return { return {
@@ -209,4 +213,121 @@ describe('ImageCanvasGenerationSubmissionModel', () => {
}), }),
).toThrow('未找到要修改的图片'); ).toThrow('未找到要修改的图片');
}); });
it('returns an icon spritesheet error when the spec reference is missing', () => {
const plan = buildIconSpritesheetGenerationSubmissionPlan({
mode: 'icon',
prompt: '',
status: 'idle',
iconDescriptions: ['返回按钮'],
});
expect(plan).toEqual({
ok: false,
errorMessage: '请选择图标素材规范',
});
});
it('returns an icon spritesheet error when descriptions are empty', () => {
const plan = buildIconSpritesheetGenerationSubmissionPlan({
mode: 'icon',
prompt: '',
status: 'idle',
iconSpecReference: {
id: 'icon-spec',
label: '图标规范',
src: 'data:image/png;base64,spec',
},
iconDescriptions: [' ', '\n'],
});
expect(plan).toEqual({
ok: false,
errorMessage: '请填写素材描述',
});
});
it('builds icon spritesheet plans with trimmed descriptions and references', () => {
const plan = buildIconSpritesheetGenerationSubmissionPlan({
mode: 'icon',
prompt: '',
status: 'idle',
imageModel: 'gpt-image-2',
aspectRatio: '3:2',
imageSize: '2K',
iconSpecReference: {
id: 'icon-spec',
label: '图标规范',
src: 'data:image/png;base64,spec',
},
iconDescriptions: [' 返回按钮 ', '', '设置按钮'],
});
expect(plan).toEqual({
ok: true,
iconDescriptions: ['返回按钮', '设置按钮'],
input: {
referenceImageSrc: 'data:image/png;base64,spec',
iconDescriptions: ['返回按钮', '设置按钮'],
model: 'gpt-image-2',
aspectRatio: '3:2',
imageSize: '2K',
},
generationInputs: {
fields: [
{ title: '素材描述 1', value: '返回按钮' },
{ title: '素材描述 2', value: '设置按钮' },
],
references: [
{
title: '图标素材规范',
label: '图标规范',
src: 'data:image/png;base64,spec',
},
],
},
rememberImageModel: 'gpt-image-2',
});
});
it('builds character animation plans with trimmed prompt and object key source', () => {
const sourceLayer = createLayer({
id: 'character-layer',
title: '角色图',
objectKey: 'generated/character.png',
assetKind: 'character',
originalWidth: 960,
originalHeight: 1280,
});
const plan = buildCharacterAnimationSubmissionPlan({
panel: {
sourceLayerId: 'character-layer',
promptText: ' 循环奔跑动作 ',
resolution: '720p',
ratio: 'same',
frameCount: 48,
durationSeconds: 6,
status: 'idle',
},
sourceLayer,
});
expect(plan).toEqual({
promptText: '循环奔跑动作',
input: {
sourceLayerId: 'character-layer',
sourceImageSrc: 'generated/character.png',
sourceWidth: 960,
sourceHeight: 1280,
promptText: '循环奔跑动作',
resolution: '720p',
ratio: 'same',
frameCount: 48,
durationSeconds: 6,
priceMudPoints: 120,
model: 'seedance2.0',
},
});
});
}); });

View File

@@ -1,21 +1,29 @@
import type { import type {
EditorCharacterAnimationGenerationInput,
EditorIconSpritesheetGenerationInput,
EditorImageGenerationInput, EditorImageGenerationInput,
} from '../../services/image-editor/editorProjectClient'; } from '../../services/image-editor/editorProjectClient';
import type { import type {
CanvasGenerationInputs, CanvasGenerationInputs,
CanvasLayer, CanvasLayer,
CharacterAnimationPanelState,
GenerateDialogState, GenerateDialogState,
} from './ImageCanvasEditorTypes'; } from './ImageCanvasEditorTypes';
import { import {
CHARACTER_ANIMATION_MODEL,
DEFAULT_IMAGE_MODEL, DEFAULT_IMAGE_MODEL,
DEFAULT_ICON_DESCRIPTIONS,
DEFAULT_SPEC_FORM_VALUES, DEFAULT_SPEC_FORM_VALUES,
SPEC_GENERATION_SIZE, SPEC_GENERATION_SIZE,
SPEC_TYPE_LABEL, SPEC_TYPE_LABEL,
buildIconGenerationInputs,
buildCharacterGenerationInputs, buildCharacterGenerationInputs,
buildEditGenerationInputs, buildEditGenerationInputs,
buildImageGenerationInputs, buildImageGenerationInputs,
buildSpecGenerationInputs, buildSpecGenerationInputs,
buildSpecPrompt, buildSpecPrompt,
calculateCharacterAnimationPrice,
resolveCharacterAnimationSourceImageSrc,
} from './ImageCanvasGenerationModel'; } from './ImageCanvasGenerationModel';
type ImageGenerationSubmissionOptions = { type ImageGenerationSubmissionOptions = {
@@ -43,6 +51,24 @@ export type ImageGenerationSubmissionPlan =
rememberImageModel?: string; rememberImageModel?: string;
}; };
export type IconSpritesheetGenerationSubmissionPlan =
| {
ok: false;
errorMessage: string;
}
| {
ok: true;
iconDescriptions: string[];
input: EditorIconSpritesheetGenerationInput;
generationInputs: CanvasGenerationInputs;
rememberImageModel: string;
};
export type CharacterAnimationSubmissionPlan = {
promptText: string;
input: EditorCharacterAnimationGenerationInput;
};
export function buildImageGenerationSubmissionPlan({ export function buildImageGenerationSubmissionPlan({
dialog, dialog,
layers, layers,
@@ -141,3 +167,72 @@ export function buildImageGenerationSubmissionPlan({
}, },
}; };
} }
export function buildIconSpritesheetGenerationSubmissionPlan(
dialog: GenerateDialogState,
): IconSpritesheetGenerationSubmissionPlan {
const iconDescriptions = (dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS)
.map((description) => description.trim())
.filter(Boolean);
if (!dialog.iconSpecReference) {
return {
ok: false,
errorMessage: '请选择图标素材规范',
};
}
if (!iconDescriptions.length) {
return {
ok: false,
errorMessage: '请填写素材描述',
};
}
const rememberImageModel = dialog.imageModel ?? DEFAULT_IMAGE_MODEL;
return {
ok: true,
iconDescriptions,
input: {
referenceImageSrc: dialog.iconSpecReference.src,
iconDescriptions,
model: rememberImageModel,
aspectRatio: dialog.aspectRatio ?? '1:1',
imageSize: dialog.imageSize ?? '1K',
},
generationInputs: buildIconGenerationInputs(
iconDescriptions,
dialog.iconSpecReference,
),
rememberImageModel,
};
}
export function buildCharacterAnimationSubmissionPlan({
panel,
sourceLayer,
}: {
panel: CharacterAnimationPanelState;
sourceLayer: CanvasLayer;
}): CharacterAnimationSubmissionPlan {
const promptText = panel.promptText.trim();
return {
promptText,
input: {
sourceLayerId: sourceLayer.id,
sourceImageSrc: resolveCharacterAnimationSourceImageSrc(sourceLayer),
sourceWidth: sourceLayer.originalWidth,
sourceHeight: sourceLayer.originalHeight,
promptText,
resolution: panel.resolution,
ratio: panel.ratio,
frameCount: panel.frameCount,
durationSeconds: panel.durationSeconds,
priceMudPoints: calculateCharacterAnimationPrice(
panel.resolution,
panel.durationSeconds,
),
model: CHARACTER_ANIMATION_MODEL,
},
};
}

View File

@@ -24,7 +24,6 @@ import {
} from './ImageCanvasGenerationLayerModel'; } from './ImageCanvasGenerationLayerModel';
import { import {
CHARACTER_ANIMATION_DURATION_OPTIONS, CHARACTER_ANIMATION_DURATION_OPTIONS,
CHARACTER_ANIMATION_MODEL,
CHARACTER_FRAME_DISPLAY_SIZE, CHARACTER_FRAME_DISPLAY_SIZE,
CHARACTER_FRAME_ORIGINAL_SIZE, CHARACTER_FRAME_ORIGINAL_SIZE,
DEFAULT_ICON_DESCRIPTIONS, DEFAULT_ICON_DESCRIPTIONS,
@@ -37,16 +36,18 @@ import {
SPEC_FRAME_DISPLAY_SIZE, SPEC_FRAME_DISPLAY_SIZE,
SPEC_FRAME_ORIGINAL_SIZE, SPEC_FRAME_ORIGINAL_SIZE,
buildEditGenerationInputs, buildEditGenerationInputs,
buildIconGenerationInputs,
buildQuickEditModelOptions, buildQuickEditModelOptions,
buildQuickEditSizeOptions, buildQuickEditSizeOptions,
calculateCharacterAnimationPrice, calculateCharacterAnimationPrice,
createCanvasLayerReference, createCanvasLayerReference,
isCanvasGenerationDialog, isCanvasGenerationDialog,
resolveCharacterAnimationSourceImageSrc,
resolveImageGenerationErrorMessage, resolveImageGenerationErrorMessage,
} from './ImageCanvasGenerationModel'; } from './ImageCanvasGenerationModel';
import { buildImageGenerationSubmissionPlan } from './ImageCanvasGenerationSubmissionModel'; import {
buildCharacterAnimationSubmissionPlan,
buildIconSpritesheetGenerationSubmissionPlan,
buildImageGenerationSubmissionPlan,
} from './ImageCanvasGenerationSubmissionModel';
import { formatImageSizeValue } from './ImageCanvasEditorModel'; import { formatImageSizeValue } from './ImageCanvasEditorModel';
import type { import type {
CanvasGenerationDialogState, CanvasGenerationDialogState,
@@ -657,29 +658,15 @@ export function useImageCanvasGenerationWorkflow({
) => { ) => {
updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog); updateCanvasGenerationDialogById(nextDialog.id, () => nextDialog);
}; };
const iconDescriptions = ( const submissionPlan =
dialog.iconDescriptions ?? DEFAULT_ICON_DESCRIPTIONS buildIconSpritesheetGenerationSubmissionPlan(dialog);
) if (!submissionPlan.ok) {
.map((description) => description.trim())
.filter(Boolean);
if (!dialog.iconSpecReference) {
if (canvasDialog) { if (canvasDialog) {
setSubmittingIconDialog({ setSubmittingIconDialog({
...canvasDialog, ...canvasDialog,
status: 'failed', status: 'failed',
composerOpen: true, composerOpen: true,
errorMessage: '请选择图标素材规范', errorMessage: submissionPlan.errorMessage,
});
}
return;
}
if (!iconDescriptions.length) {
if (canvasDialog) {
setSubmittingIconDialog({
...canvasDialog,
status: 'failed',
composerOpen: true,
errorMessage: '请填写素材描述',
}); });
} }
return; return;
@@ -691,32 +678,28 @@ export function useImageCanvasGenerationWorkflow({
setSubmittingIconDialog({ setSubmittingIconDialog({
...canvasDialog, ...canvasDialog,
iconDescriptions, iconDescriptions: submissionPlan.iconDescriptions,
status: 'generating', status: 'generating',
composerOpen: false, composerOpen: false,
errorMessage: undefined, errorMessage: undefined,
}); });
try { try {
const generated = await generateEditorIconSpritesheet({ const generated = await generateEditorIconSpritesheet(
referenceImageSrc: dialog.iconSpecReference.src, submissionPlan.input,
iconDescriptions, );
model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL, setLastImageModel(submissionPlan.rememberImageModel);
aspectRatio: dialog.aspectRatio ?? '1:1',
imageSize: dialog.imageSize ?? '1K',
});
setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL);
addIconSpritesheetResultLayers( addIconSpritesheetResultLayers(
generated, generated,
generated.iconImageSrcs, generated.iconImageSrcs,
buildIconGenerationInputs(iconDescriptions, dialog.iconSpecReference), submissionPlan.generationInputs,
getGeneratingDialogPlaceholder(dialog), getGeneratingDialogPlaceholder(dialog),
canvasDialog.id, canvasDialog.id,
); );
} catch (error) { } catch (error) {
setSubmittingIconDialog({ setSubmittingIconDialog({
...canvasDialog, ...canvasDialog,
iconDescriptions, iconDescriptions: submissionPlan.iconDescriptions,
status: 'failed', status: 'failed',
composerOpen: true, composerOpen: true,
errorMessage: resolveImageGenerationErrorMessage(error), errorMessage: resolveImageGenerationErrorMessage(error),
@@ -913,10 +896,13 @@ export function useImageCanvasGenerationWorkflow({
if (!characterAnimationPanel || !characterAnimationSourceLayer) { if (!characterAnimationPanel || !characterAnimationSourceLayer) {
return; return;
} }
const promptText = characterAnimationPanel.promptText.trim(); const submissionPlan = buildCharacterAnimationSubmissionPlan({
panel: characterAnimationPanel,
sourceLayer: characterAnimationSourceLayer,
});
const nextPanel = { const nextPanel = {
...characterAnimationPanel, ...characterAnimationPanel,
promptText, promptText: submissionPlan.promptText,
status: 'generating' as const, status: 'generating' as const,
errorMessage: undefined, errorMessage: undefined,
result: undefined, result: undefined,
@@ -924,24 +910,9 @@ export function useImageCanvasGenerationWorkflow({
setCharacterAnimationPanel(nextPanel); setCharacterAnimationPanel(nextPanel);
try { try {
const result = await generateEditorCharacterAnimation({ const result = await generateEditorCharacterAnimation(
sourceLayerId: characterAnimationSourceLayer.id, submissionPlan.input,
sourceImageSrc: resolveCharacterAnimationSourceImageSrc( );
characterAnimationSourceLayer,
),
sourceWidth: characterAnimationSourceLayer.originalWidth,
sourceHeight: characterAnimationSourceLayer.originalHeight,
promptText,
resolution: nextPanel.resolution,
ratio: nextPanel.ratio,
frameCount: nextPanel.frameCount,
durationSeconds: nextPanel.durationSeconds,
priceMudPoints: calculateCharacterAnimationPrice(
nextPanel.resolution,
nextPanel.durationSeconds,
),
model: CHARACTER_ANIMATION_MODEL,
});
setCharacterAnimationPanel((currentPanel) => setCharacterAnimationPanel((currentPanel) =>
currentPanel currentPanel
? { ? {