From 05758775e846d33b82d112a630d29588cdec6682 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 19 Jun 2026 17:24:00 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=92=E8=89=B2=E9=87=8D=E7=BB=98=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E8=A7=92=E8=89=B2=E6=8F=90=E7=A4=BA=E8=AF=8D=E9=99=90?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 角色图层重绘提交为角色生成类型 补充角色重绘请求契约测试 更新图片画布重绘链路文档 --- ...架构】图片画布编辑器MVP接入方案-2026-06-11.md | 2 +- ...anvasGenerationSubmissionWorkflow.test.tsx | 97 +++++++++++++++---- ...ImageCanvasGenerationSubmissionWorkflow.ts | 4 +- 3 files changed, 80 insertions(+), 23 deletions(-) diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index 0e3dd7bed..e95582712 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -18,7 +18,7 @@ - 图片拖拽时显示水平 / 垂直吸附参考线,吸附到其它图层或画板的边缘与中心线。 - 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 - 对生成资源执行重绘时,在右侧创建新的生成结果图层,并自动调整视图显示原图和新图;重绘面板不因提交成功自动关闭,便于连续改提示词。 -- 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;快速编辑按原图模型提交;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。纯文本生成走 `/api/editor/images/generations`,重绘在前端读入当前图层图片 Data URL 后作为 `quick-edit` 参考图走同一图片生成 BFF,并在原图右侧生成一张新图。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;首期固定文字转视频、`16:9`、标准模式、静音,生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。快速编辑和重绘在调用后端前必须把当前图层图片源读取为图片 Data URL,来源可以是本地上传 Data URL、站内 public 图片、历史 `/generated-*` 路径或可读取的 OSS generated URL;后端仍只接收图片 Data URL,不把普通 URL 直接透传到 VectorEngine edits。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 +- 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;快速编辑按原图模型提交;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。纯文本生成走 `/api/editor/images/generations`,重绘在前端读入当前图层图片 Data URL 后走同一图片生成 BFF,并在原图右侧生成一张新图;普通图层重绘作为 `quick-edit` 参考图提交,角色图层重绘必须按 `kind: "character"` 提交,继续套用角色生成器提示词限定、透明 PNG 后处理和角色资产持久化。`生成视频` 走 `/api/editor/videos/generations`,前端模型入口仅展示 Seedance 2.0 Fast / Seedance 2.0 / Kling 3.0 / Kling 3.0 Omni,不展示 Veo 入口,默认 Seedance 2.0 Fast;首期固定文字转视频、`16:9`、标准模式、静音,生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。快速编辑和重绘在调用后端前必须把当前图层图片源读取为图片 Data URL,来源可以是本地上传 Data URL、站内 public 图片、历史 `/generated-*` 路径或可读取的 OSS generated URL;后端仍只接收图片 Data URL,不把普通 URL 直接透传到 VectorEngine edits。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 - 快速编辑面板对齐其它生成类面板:首行支持额外参考图,最多 8 张;原图自动作为最后一张参考图展示和提交。尺寸、模型沿用原图配置,只以禁用态参数胶囊展示,不可点击修改。提交前将提示词里对原图的 `原图`、`当前图片`、`当前图` 或 `图1` 引用改写为最后一张原图编号,例如额外 1 张参考图时写成 `图2`。点击快速编辑生成后立即创建独立 `Quick Edit Generator` 画布占位播放生成中动画,不再在原图图层上播放生成中遮罩;该生成中占位可通过键盘 `Delete` / `Backspace` 删除。 - 底部生成类按钮每次点击都必须创建独立的画布生成对象;新建规范、角色形象或图标素材时,只切换当前编辑面板,不得销毁此前尚未生成或已生成后的其它生成对象状态。归档为非当前编辑对象的生成占位仍可拖动、删除和等待异步完成,完成 / 失败回写必须按生成对象 ID 读取最新占位状态,不能使用提交瞬间的旧快照。 - 所有会新建画布生成占位的入口必须先创建 draft,再统一经过 `ImageCanvasGenerationPlacementModel` 计算落点,禁止各入口自行使用当前视口中心裸坐标。当前覆盖入口包括 `生成图片`、`生成规范`、`生成角色形象`、`生成图标素材`、`生成视频`、`生成UI设计图` 和 `生成角色动作`。placement 模型的避让对象为所有未隐藏画布图层,以及当前 active / inactive generation dialogs 中仍存在的 placeholder;每个避让矩形按 32px 画布世界坐标间距外扩。候选落点以当前视口世界中心为距离目标,优先选择离视口中心最近且不重叠的占位位置;若中心被占用,会按上下左右和环形候选继续寻找。打开生成面板时必须把避让后的 placeholder 写入 `openCanvasGenerationDialog(...)`,并立即调用 `centerViewportOnPlacement(...)` 居中到新占位中心,保持原 viewport scale 不变。 diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx index 47fb7cd59..67bc95173 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx @@ -323,13 +323,15 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { 'quick-edit:generating:closed:-:placeholder:-', ); await waitFor(() => { - expect(generateEditorImageMock).toHaveBeenCalledWith({ - prompt: '快速修图', - size: '1024x768', - kind: 'quick-edit', - model: 'gemini-3.1-flash-image-preview', - referenceImageSrcs: ['data:image/png;base64,source.png'], - }); + expect(generateEditorImageMock).toHaveBeenCalledWith( + expect.objectContaining({ + prompt: '快速修图', + size: '1024x768', + kind: 'quick-edit', + model: 'gemini-3.1-flash-image-preview', + referenceImageSrcs: ['data:image/png;base64,source.png'], + }), + ); }); await waitFor(() => { expect(screen.getByTestId('layers').textContent).toContain( @@ -368,6 +370,53 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { }); }); + it('submits character redraws through the character image prompt contract', async () => { + generateEditorImageMock.mockResolvedValueOnce( + createGenerated({ prompt: '角色换成蓝色披风' }), + ); + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' })); + + await waitFor(() => { + expect(generateEditorImageMock).toHaveBeenCalledWith( + expect.objectContaining({ + prompt: '角色换成蓝色披风', + size: '1024x768', + kind: 'character', + model: 'gpt-image-2', + referenceImageSrcs: ['data:image/png;base64,source.png'], + assetKind: 'character', + }), + ); + }); + await waitFor(() => { + expect(screen.getByTestId('layers').textContent).toContain( + 'layer-quick-edit-1:源图 重绘:resource-source:character', + ); + }); + expect(screen.getByTestId('quick-edit').textContent).toBe( + 'layer-source:idle:角色换成蓝色披风:-', + ); + }); + it('submits edit dialogs beside the source and clears the modal state', async () => { editEditorImageMock.mockResolvedValueOnce( createGenerated({ prompt: '修改当前图片' }), @@ -390,10 +439,12 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { 'edit:generating:open:-:-:-', ); await waitFor(() => { - expect(editEditorImageMock).toHaveBeenCalledWith({ - prompt: '修改当前图片', - sourceImageSrc: 'data:image/png;base64,source.png', - }); + expect(editEditorImageMock).toHaveBeenCalledWith( + expect.objectContaining({ + prompt: '修改当前图片', + sourceImageSrc: 'data:image/png;base64,source.png', + }), + ); }); await waitFor(() => { expect(screen.getByTestId('layers').textContent).toContain( @@ -492,13 +543,15 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { 'publication:generating:closed:-:placeholder:-', ); await waitFor(() => { - expect(generateEditorImageMock).toHaveBeenCalledWith({ - prompt: expect.stringContaining('【宣发素材类型】详情页单图'), - size: '720x1280', - kind: 'publication-material', - model: 'gemini-3.1-flash-image-preview', - referenceImageSrcs: ['data:image/png;base64,large-ref'], - }); + expect(generateEditorImageMock).toHaveBeenCalledWith( + expect.objectContaining({ + prompt: expect.stringContaining('【宣发素材类型】详情页单图'), + size: '720x1280', + kind: 'publication-material', + model: 'gemini-3.1-flash-image-preview', + referenceImageSrcs: ['data:image/png;base64,large-ref'], + }), + ); }); const prompt = generateEditorImageMock.mock.calls[0]?.[0]?.prompt ?? ''; expect(prompt).toContain('游戏名:云朵消消乐'); @@ -707,9 +760,11 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { ); }); await waitFor(() => { - expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith({ - sourceImageSrc: 'data:image/png;base64,ui-design.png', - }); + expect(extractEditorUiDesignAssetsMock).toHaveBeenCalledWith( + expect.objectContaining({ + sourceImageSrc: 'data:image/png;base64,ui-design.png', + }), + ); }); await waitFor(() => { expect(screen.getByTestId('layers').textContent).toContain( diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts index 35bdfe346..b8d1881c9 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts @@ -697,10 +697,12 @@ export function useImageCanvasGenerationSubmissionWorkflow({ quickEditSourceLayer, quickEditReferences, ); + const isCharacterRedraw = + panelMode === 'redraw' && quickEditSourceLayer.assetKind === 'character'; const generated = await generateEditorImage({ prompt: normalizedPrompt, size: quickEditPanel.size, - kind: 'quick-edit', + kind: isCharacterRedraw ? 'character' : 'quick-edit', model: normalizeEditorImageModel(quickEditPanel.model), referenceImageSrcs: [ ...extraReferenceImageSrcs,