拆分编辑器生成提交模型
抽出图片生成请求与结果快照构建逻辑 补充生成提交模型单测 更新 TRACKING.md 记录第三十五阶段验证
This commit is contained in:
@@ -151,3 +151,4 @@
|
|||||||
- 2026-06-17 前端拆分第三十二阶段:继续收口 `ImageCanvasGenerationComposerView`,新增 `ImageCanvasBasicGenerationComposerView`、`ImageCanvasCharacterGenerationComposerView` 和 `ImageCanvasEditGenerationModalView`,把普通生图跟随框、角色形象生成面板和修改图片弹窗从 Composer 内联 JSX 中抽出;Composer 降至 312 行,只保留生成模式分流、portal 菜单和各面板装配。新增三组子视图单测覆盖普通生图 prompt / 参考图 / 提交 / 关闭、角色参考图菜单 / 状态恢复 / 提交、修改图片弹窗提示词 / 失败 / 关闭。统一验证命令:`npm run test -- 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/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 打开成功,未登录显示 `账号入口` 且控制台仅有预期 `/api/auth/refresh` 401;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,`AI画布工具栏` 仍可见;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见。
|
- 2026-06-17 前端拆分第三十二阶段:继续收口 `ImageCanvasGenerationComposerView`,新增 `ImageCanvasBasicGenerationComposerView`、`ImageCanvasCharacterGenerationComposerView` 和 `ImageCanvasEditGenerationModalView`,把普通生图跟随框、角色形象生成面板和修改图片弹窗从 Composer 内联 JSX 中抽出;Composer 降至 312 行,只保留生成模式分流、portal 菜单和各面板装配。新增三组子视图单测覆盖普通生图 prompt / 参考图 / 提交 / 关闭、角色参考图菜单 / 状态恢复 / 提交、修改图片弹窗提示词 / 失败 / 关闭。统一验证命令:`npm run test -- 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/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 打开成功,未登录显示 `账号入口` 且控制台仅有预期 `/api/auth/refresh` 401;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`,`AI画布工具栏` 仍可见;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见。
|
||||||
- 2026-06-17 前端拆分第三十三阶段:继续收口 `ImageCanvasAssetLibraryPanelView`,新增 `ImageCanvasAssetFolderSectionView` 和 `ImageCanvasAssetRowView`,把素材库文件夹头 / 文件夹 drop 区域、素材卡片 / 上传进度 / 重命名 / 选择模式从素材库父面板中拆成两个完整 surface;素材库父面板降至 279 行,只保留素材列表容器、新建文件夹表单、批量操作栏和框选遮罩。新增素材行单测覆盖普通点击加入画布、选择模式改为选中、重命名 Enter 提交和上传中禁用 / 进度显示。统一验证命令:`npm run test -- 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/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 打开成功,未登录显示 `账号入口` 且控制台仅有预期 `/api/auth/refresh` 401;默认素材栏显示 `项目素材` 文件夹、上传入口和底部 `AI画布工具栏`;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见。
|
- 2026-06-17 前端拆分第三十三阶段:继续收口 `ImageCanvasAssetLibraryPanelView`,新增 `ImageCanvasAssetFolderSectionView` 和 `ImageCanvasAssetRowView`,把素材库文件夹头 / 文件夹 drop 区域、素材卡片 / 上传进度 / 重命名 / 选择模式从素材库父面板中拆成两个完整 surface;素材库父面板降至 279 行,只保留素材列表容器、新建文件夹表单、批量操作栏和框选遮罩。新增素材行单测覆盖普通点击加入画布、选择模式改为选中、重命名 Enter 提交和上传中禁用 / 进度显示。统一验证命令:`npm run test -- 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/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 打开成功,未登录显示 `账号入口` 且控制台仅有预期 `/api/auth/refresh` 401;默认素材栏显示 `项目素材` 文件夹、上传入口和底部 `AI画布工具栏`;关闭登录后 `画布背景设置` 可打开,点击 `暖灰` 后 viewport 背景为 `rgb(243, 240, 234)` 且 `background-image: none`;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见。
|
||||||
- 2026-06-17 前端拆分第三十四阶段:继续收口 `ImageCanvasStageView`,新增 `ImageCanvasWorldView`,把画布世界表面、吸附参考线、可见图层排序 / 悬浮 / 选中 / 锁定 / 生成态、元数据角标、框选矩形、生成占位框和浮动生成状态从 StageView 内联 JSX 中抽出;StageView 降至 324 行,继续保留 viewport 宿主、drop overlay、左下 dock、底部工具栏、右键菜单和选中图片工具栏装配。新增 world view 单测覆盖隐藏图层过滤、悬浮尺寸、生成态、元数据按钮、吸附线、框选矩形和生成占位框事件。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasWorldView.test.tsx`、`npm run test -- 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`,`AI画布工具栏` 保持可见;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见,控制台仅有预期的未登录 `/api/auth/refresh` 401。
|
- 2026-06-17 前端拆分第三十四阶段:继续收口 `ImageCanvasStageView`,新增 `ImageCanvasWorldView`,把画布世界表面、吸附参考线、可见图层排序 / 悬浮 / 选中 / 锁定 / 生成态、元数据角标、框选矩形、生成占位框和浮动生成状态从 StageView 内联 JSX 中抽出;StageView 降至 324 行,继续保留 viewport 宿主、drop overlay、左下 dock、底部工具栏、右键菜单和选中图片工具栏装配。新增 world view 单测覆盖隐藏图层过滤、悬浮尺寸、生成态、元数据按钮、吸附线、框选矩形和生成占位框事件。统一验证命令:`npm run test -- src/components/image-editor/ImageCanvasWorldView.test.tsx`、`npm run test -- 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`,`AI画布工具栏` 保持可见;`打开图层` 切换后侧栏显示 `图层`;点击 `生成工具` 后 `Image Generator`、`生成图片` 对话框和底部工具栏均可见,控制台仅有预期的未登录 `/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。
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import type { CanvasLayer } from './ImageCanvasEditorTypes';
|
||||||
|
import { buildImageGenerationSubmissionPlan } from './ImageCanvasGenerationSubmissionModel';
|
||||||
|
|
||||||
|
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('ImageCanvasGenerationSubmissionModel', () => {
|
||||||
|
it('builds normal image generation submission plans', () => {
|
||||||
|
const plan = buildImageGenerationSubmissionPlan({
|
||||||
|
dialog: {
|
||||||
|
mode: 'generate',
|
||||||
|
prompt: ' 一张发光主视觉 ',
|
||||||
|
status: 'idle',
|
||||||
|
},
|
||||||
|
layers: [],
|
||||||
|
nextGeneratedIndex: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plan).toEqual({
|
||||||
|
kind: 'image',
|
||||||
|
normalizedPrompt: '一张发光主视觉',
|
||||||
|
input: {
|
||||||
|
prompt: '一张发光主视觉',
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
generationInputs: {
|
||||||
|
fields: [{ title: '生成提示词', value: '一张发光主视觉' }],
|
||||||
|
references: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds edit submission plans with the source layer snapshot', () => {
|
||||||
|
const sourceLayer = createLayer();
|
||||||
|
|
||||||
|
const plan = buildImageGenerationSubmissionPlan({
|
||||||
|
dialog: {
|
||||||
|
mode: 'edit',
|
||||||
|
prompt: '',
|
||||||
|
status: 'idle',
|
||||||
|
sourceLayerId: sourceLayer.id,
|
||||||
|
},
|
||||||
|
layers: [sourceLayer],
|
||||||
|
nextGeneratedIndex: 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plan).toMatchObject({
|
||||||
|
kind: 'edit',
|
||||||
|
normalizedPrompt: '修改当前图片',
|
||||||
|
sourceLayer,
|
||||||
|
generationInputs: {
|
||||||
|
fields: [{ title: '修改要求', value: '修改当前图片' }],
|
||||||
|
references: [
|
||||||
|
{
|
||||||
|
title: '参考图',
|
||||||
|
label: '源图',
|
||||||
|
src: 'data:image/png;base64,source',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds spec generation plans with reference prompt semantics', () => {
|
||||||
|
const plan = buildImageGenerationSubmissionPlan({
|
||||||
|
dialog: {
|
||||||
|
mode: 'spec',
|
||||||
|
prompt: '',
|
||||||
|
status: 'idle',
|
||||||
|
specType: 'icon',
|
||||||
|
specValues: {
|
||||||
|
playSetting: '休闲消除',
|
||||||
|
artStyle: '清爽卡通',
|
||||||
|
bodyRatio: '3',
|
||||||
|
characterView: '',
|
||||||
|
customPrompt: '',
|
||||||
|
},
|
||||||
|
specReference: {
|
||||||
|
id: 'spec-ref',
|
||||||
|
label: '参考.png',
|
||||||
|
src: 'data:image/png;base64,ref',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
layers: [],
|
||||||
|
nextGeneratedIndex: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plan).toMatchObject({
|
||||||
|
kind: 'image',
|
||||||
|
normalizedPrompt: 'AI 生成图片',
|
||||||
|
input: {
|
||||||
|
size: '2048x1152',
|
||||||
|
kind: 'spec',
|
||||||
|
referenceImageSrcs: ['data:image/png;base64,ref'],
|
||||||
|
prompt: expect.stringContaining('参考图生成规范'),
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
assetKind: 'icon-spec',
|
||||||
|
title: '图标素材规范 8',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({
|
||||||
|
fields: [
|
||||||
|
{ title: '玩法设定', value: '休闲消除' },
|
||||||
|
{ title: '美术风格', value: '清爽卡通' },
|
||||||
|
],
|
||||||
|
references: [
|
||||||
|
{
|
||||||
|
title: '参考图',
|
||||||
|
label: '参考.png',
|
||||||
|
src: 'data:image/png;base64,ref',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds character plans with references and remembered model', () => {
|
||||||
|
const plan = buildImageGenerationSubmissionPlan({
|
||||||
|
dialog: {
|
||||||
|
mode: 'character',
|
||||||
|
prompt: ' 白发骑士 ',
|
||||||
|
status: 'idle',
|
||||||
|
imageModel: 'gpt-image-2',
|
||||||
|
aspectRatio: '2:3',
|
||||||
|
imageSize: '2K',
|
||||||
|
characterSpecReference: {
|
||||||
|
id: 'spec',
|
||||||
|
label: '角色规范',
|
||||||
|
src: 'data:image/png;base64,spec',
|
||||||
|
},
|
||||||
|
characterReferences: [
|
||||||
|
{
|
||||||
|
id: 'ref-1',
|
||||||
|
label: '盔甲参考',
|
||||||
|
src: 'data:image/png;base64,armor',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
layers: [],
|
||||||
|
nextGeneratedIndex: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(plan).toMatchObject({
|
||||||
|
kind: 'image',
|
||||||
|
normalizedPrompt: '白发骑士',
|
||||||
|
input: {
|
||||||
|
prompt: '白发骑士',
|
||||||
|
kind: 'character',
|
||||||
|
model: 'gpt-image-2',
|
||||||
|
aspectRatio: '2:3',
|
||||||
|
imageSize: '2K',
|
||||||
|
referenceImageSrcs: [
|
||||||
|
'data:image/png;base64,spec',
|
||||||
|
'data:image/png;base64,armor',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
assetKind: 'character',
|
||||||
|
title: '角色形象 9',
|
||||||
|
},
|
||||||
|
rememberImageModel: 'gpt-image-2',
|
||||||
|
});
|
||||||
|
expect(plan.kind === 'image' ? plan.result.generationInputs : null).toEqual({
|
||||||
|
fields: [{ title: '角色设定', value: '白发骑士' }],
|
||||||
|
references: [
|
||||||
|
{
|
||||||
|
title: '角色形象规范',
|
||||||
|
label: '角色规范',
|
||||||
|
src: 'data:image/png;base64,spec',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '常规参考图 1',
|
||||||
|
label: '盔甲参考',
|
||||||
|
src: 'data:image/png;base64,armor',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when edit source layer is missing', () => {
|
||||||
|
expect(() =>
|
||||||
|
buildImageGenerationSubmissionPlan({
|
||||||
|
dialog: {
|
||||||
|
mode: 'edit',
|
||||||
|
prompt: '修图',
|
||||||
|
status: 'idle',
|
||||||
|
sourceLayerId: 'missing-layer',
|
||||||
|
},
|
||||||
|
layers: [],
|
||||||
|
nextGeneratedIndex: 1,
|
||||||
|
}),
|
||||||
|
).toThrow('未找到要修改的图片');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
import type {
|
||||||
|
EditorImageGenerationInput,
|
||||||
|
} from '../../services/image-editor/editorProjectClient';
|
||||||
|
import type {
|
||||||
|
CanvasGenerationInputs,
|
||||||
|
CanvasLayer,
|
||||||
|
GenerateDialogState,
|
||||||
|
} from './ImageCanvasEditorTypes';
|
||||||
|
import {
|
||||||
|
DEFAULT_IMAGE_MODEL,
|
||||||
|
DEFAULT_SPEC_FORM_VALUES,
|
||||||
|
SPEC_GENERATION_SIZE,
|
||||||
|
SPEC_TYPE_LABEL,
|
||||||
|
buildCharacterGenerationInputs,
|
||||||
|
buildEditGenerationInputs,
|
||||||
|
buildImageGenerationInputs,
|
||||||
|
buildSpecGenerationInputs,
|
||||||
|
buildSpecPrompt,
|
||||||
|
} from './ImageCanvasGenerationModel';
|
||||||
|
|
||||||
|
type ImageGenerationSubmissionOptions = {
|
||||||
|
dialog: GenerateDialogState;
|
||||||
|
layers: CanvasLayer[];
|
||||||
|
nextGeneratedIndex: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ImageGenerationSubmissionPlan =
|
||||||
|
| {
|
||||||
|
kind: 'edit';
|
||||||
|
normalizedPrompt: string;
|
||||||
|
sourceLayer: CanvasLayer;
|
||||||
|
generationInputs: CanvasGenerationInputs;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
kind: 'image';
|
||||||
|
normalizedPrompt: string;
|
||||||
|
input: EditorImageGenerationInput;
|
||||||
|
result: {
|
||||||
|
assetKind?: CanvasLayer['assetKind'];
|
||||||
|
title?: string;
|
||||||
|
generationInputs: CanvasGenerationInputs;
|
||||||
|
};
|
||||||
|
rememberImageModel?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildImageGenerationSubmissionPlan({
|
||||||
|
dialog,
|
||||||
|
layers,
|
||||||
|
nextGeneratedIndex,
|
||||||
|
}: ImageGenerationSubmissionOptions): ImageGenerationSubmissionPlan {
|
||||||
|
const normalizedPrompt =
|
||||||
|
dialog.prompt.trim() ||
|
||||||
|
(dialog.mode === 'edit' ? '修改当前图片' : 'AI 生成图片');
|
||||||
|
|
||||||
|
if (dialog.mode === 'edit') {
|
||||||
|
const sourceLayer = layers.find((layer) => layer.id === dialog.sourceLayerId);
|
||||||
|
if (!sourceLayer) {
|
||||||
|
throw new Error('未找到要修改的图片');
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: 'edit',
|
||||||
|
normalizedPrompt,
|
||||||
|
sourceLayer,
|
||||||
|
generationInputs: buildEditGenerationInputs(
|
||||||
|
'修改要求',
|
||||||
|
normalizedPrompt,
|
||||||
|
sourceLayer,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialog.mode === 'spec') {
|
||||||
|
const specType = dialog.specType ?? 'custom';
|
||||||
|
const specValues = dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType];
|
||||||
|
return {
|
||||||
|
kind: 'image',
|
||||||
|
normalizedPrompt,
|
||||||
|
input: {
|
||||||
|
prompt: buildSpecPrompt(
|
||||||
|
specType,
|
||||||
|
specValues,
|
||||||
|
Boolean(dialog.specReference?.src),
|
||||||
|
),
|
||||||
|
size: SPEC_GENERATION_SIZE,
|
||||||
|
model: DEFAULT_IMAGE_MODEL,
|
||||||
|
kind: 'spec',
|
||||||
|
...(dialog.specReference?.src
|
||||||
|
? { referenceImageSrcs: [dialog.specReference.src] }
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
assetKind: specType === 'icon' ? 'icon-spec' : 'spec',
|
||||||
|
title: `${SPEC_TYPE_LABEL[specType]} ${nextGeneratedIndex}`,
|
||||||
|
generationInputs: buildSpecGenerationInputs(
|
||||||
|
specType,
|
||||||
|
specValues,
|
||||||
|
dialog.specReference,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dialog.mode === 'character') {
|
||||||
|
const referenceImageSrcs = [
|
||||||
|
dialog.characterSpecReference?.src,
|
||||||
|
...(dialog.characterReferences ?? []).map((reference) => reference.src),
|
||||||
|
].filter((src): src is string => Boolean(src));
|
||||||
|
const imageModel = dialog.imageModel ?? DEFAULT_IMAGE_MODEL;
|
||||||
|
return {
|
||||||
|
kind: 'image',
|
||||||
|
normalizedPrompt,
|
||||||
|
input: {
|
||||||
|
prompt: normalizedPrompt,
|
||||||
|
kind: 'character',
|
||||||
|
model: imageModel,
|
||||||
|
aspectRatio: dialog.aspectRatio ?? '1:1',
|
||||||
|
imageSize: dialog.imageSize ?? '1K',
|
||||||
|
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
assetKind: 'character',
|
||||||
|
title: `角色形象 ${nextGeneratedIndex}`,
|
||||||
|
generationInputs: buildCharacterGenerationInputs(
|
||||||
|
normalizedPrompt,
|
||||||
|
dialog.characterSpecReference,
|
||||||
|
dialog.characterReferences,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
rememberImageModel: imageModel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: 'image',
|
||||||
|
normalizedPrompt,
|
||||||
|
input: {
|
||||||
|
prompt: normalizedPrompt,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
generationInputs: buildImageGenerationInputs(normalizedPrompt),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -36,22 +36,17 @@ import {
|
|||||||
ICON_FRAME_ORIGINAL_SIZE,
|
ICON_FRAME_ORIGINAL_SIZE,
|
||||||
SPEC_FRAME_DISPLAY_SIZE,
|
SPEC_FRAME_DISPLAY_SIZE,
|
||||||
SPEC_FRAME_ORIGINAL_SIZE,
|
SPEC_FRAME_ORIGINAL_SIZE,
|
||||||
SPEC_GENERATION_SIZE,
|
|
||||||
SPEC_TYPE_LABEL,
|
|
||||||
buildCharacterGenerationInputs,
|
|
||||||
buildEditGenerationInputs,
|
buildEditGenerationInputs,
|
||||||
buildIconGenerationInputs,
|
buildIconGenerationInputs,
|
||||||
buildImageGenerationInputs,
|
|
||||||
buildQuickEditModelOptions,
|
buildQuickEditModelOptions,
|
||||||
buildQuickEditSizeOptions,
|
buildQuickEditSizeOptions,
|
||||||
buildSpecGenerationInputs,
|
|
||||||
buildSpecPrompt,
|
|
||||||
calculateCharacterAnimationPrice,
|
calculateCharacterAnimationPrice,
|
||||||
createCanvasLayerReference,
|
createCanvasLayerReference,
|
||||||
isCanvasGenerationDialog,
|
isCanvasGenerationDialog,
|
||||||
resolveCharacterAnimationSourceImageSrc,
|
resolveCharacterAnimationSourceImageSrc,
|
||||||
resolveImageGenerationErrorMessage,
|
resolveImageGenerationErrorMessage,
|
||||||
} from './ImageCanvasGenerationModel';
|
} from './ImageCanvasGenerationModel';
|
||||||
|
import { buildImageGenerationSubmissionPlan } from './ImageCanvasGenerationSubmissionModel';
|
||||||
import { formatImageSizeValue } from './ImageCanvasEditorModel';
|
import { formatImageSizeValue } from './ImageCanvasEditorModel';
|
||||||
import type {
|
import type {
|
||||||
CanvasGenerationDialogState,
|
CanvasGenerationDialogState,
|
||||||
@@ -801,92 +796,34 @@ export function useImageCanvasGenerationWorkflow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (dialog.mode === 'edit') {
|
const submissionPlan = buildImageGenerationSubmissionPlan({
|
||||||
const sourceLayer = layers.find(
|
dialog,
|
||||||
(layer) => layer.id === dialog.sourceLayerId,
|
layers,
|
||||||
);
|
nextGeneratedIndex: layerCounterRef.current + 1,
|
||||||
if (!sourceLayer) {
|
});
|
||||||
throw new Error('未找到要修改的图片');
|
if (submissionPlan.kind === 'edit') {
|
||||||
}
|
|
||||||
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
const referenceImageSrc = await resolveEditorImageReferenceDataUrl(
|
||||||
sourceLayer.src,
|
submissionPlan.sourceLayer.src,
|
||||||
);
|
);
|
||||||
const generated = await editEditorImage({
|
const generated = await editEditorImage({
|
||||||
prompt: normalizedPrompt,
|
prompt: submissionPlan.normalizedPrompt,
|
||||||
sourceImageSrc: referenceImageSrc,
|
sourceImageSrc: referenceImageSrc,
|
||||||
});
|
});
|
||||||
addGeneratedResultLayer(generated, {
|
addGeneratedResultLayer(generated, {
|
||||||
sourceLayer,
|
sourceLayer: submissionPlan.sourceLayer,
|
||||||
generationInputs: buildEditGenerationInputs(
|
generationInputs: submissionPlan.generationInputs,
|
||||||
'修改要求',
|
|
||||||
normalizedPrompt,
|
|
||||||
sourceLayer,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} else if (dialog.mode === 'spec') {
|
|
||||||
const specType = dialog.specType ?? 'custom';
|
|
||||||
const specValues =
|
|
||||||
dialog.specValues ?? DEFAULT_SPEC_FORM_VALUES[specType];
|
|
||||||
const specPrompt = buildSpecPrompt(
|
|
||||||
specType,
|
|
||||||
specValues,
|
|
||||||
Boolean(dialog.specReference?.src),
|
|
||||||
);
|
|
||||||
const generated = await generateEditorImage({
|
|
||||||
prompt: specPrompt,
|
|
||||||
size: SPEC_GENERATION_SIZE,
|
|
||||||
model: DEFAULT_IMAGE_MODEL,
|
|
||||||
kind: 'spec',
|
|
||||||
...(dialog.specReference?.src
|
|
||||||
? { referenceImageSrcs: [dialog.specReference.src] }
|
|
||||||
: {}),
|
|
||||||
});
|
|
||||||
addGeneratedResultLayer(generated, {
|
|
||||||
frame: getGeneratingDialogPlaceholder(dialog),
|
|
||||||
assetKind: specType === 'icon' ? 'icon-spec' : 'spec',
|
|
||||||
title: `${SPEC_TYPE_LABEL[specType]} ${layerCounterRef.current + 1}`,
|
|
||||||
dialogId: canvasDialog?.id,
|
|
||||||
generationInputs: buildSpecGenerationInputs(
|
|
||||||
specType,
|
|
||||||
specValues,
|
|
||||||
dialog.specReference,
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} else if (dialog.mode === 'character') {
|
|
||||||
const referenceImageSrcs = [
|
|
||||||
dialog.characterSpecReference?.src,
|
|
||||||
...(dialog.characterReferences ?? []).map(
|
|
||||||
(reference) => reference.src,
|
|
||||||
),
|
|
||||||
].filter((src): src is string => Boolean(src));
|
|
||||||
const generated = await generateEditorImage({
|
|
||||||
prompt: normalizedPrompt,
|
|
||||||
kind: 'character',
|
|
||||||
model: dialog.imageModel ?? DEFAULT_IMAGE_MODEL,
|
|
||||||
aspectRatio: dialog.aspectRatio ?? '1:1',
|
|
||||||
imageSize: dialog.imageSize ?? '1K',
|
|
||||||
...(referenceImageSrcs.length ? { referenceImageSrcs } : {}),
|
|
||||||
});
|
|
||||||
setLastImageModel(dialog.imageModel ?? DEFAULT_IMAGE_MODEL);
|
|
||||||
addGeneratedResultLayer(generated, {
|
|
||||||
frame: getGeneratingDialogPlaceholder(dialog),
|
|
||||||
assetKind: 'character',
|
|
||||||
title: `角色形象 ${layerCounterRef.current + 1}`,
|
|
||||||
dialogId: canvasDialog?.id,
|
|
||||||
generationInputs: buildCharacterGenerationInputs(
|
|
||||||
normalizedPrompt,
|
|
||||||
dialog.characterSpecReference,
|
|
||||||
dialog.characterReferences,
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const generated = await generateEditorImage({
|
const generated = await generateEditorImage(submissionPlan.input);
|
||||||
prompt: normalizedPrompt,
|
if (submissionPlan.rememberImageModel) {
|
||||||
});
|
setLastImageModel(submissionPlan.rememberImageModel);
|
||||||
|
}
|
||||||
addGeneratedResultLayer(generated, {
|
addGeneratedResultLayer(generated, {
|
||||||
frame: getGeneratingDialogPlaceholder(dialog),
|
frame: getGeneratingDialogPlaceholder(dialog),
|
||||||
|
assetKind: submissionPlan.result.assetKind,
|
||||||
|
title: submissionPlan.result.title,
|
||||||
dialogId: canvasDialog?.id,
|
dialogId: canvasDialog?.id,
|
||||||
generationInputs: buildImageGenerationInputs(normalizedPrompt),
|
generationInputs: submissionPlan.result.generationInputs,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user