diff --git a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md index f1b999b56..57dac1f1d 100644 --- a/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md +++ b/docs/technical/【前端架构】图片画布编辑器MVP接入方案-2026-06-11.md @@ -21,7 +21,7 @@ - 生成资源右上角显示元数据按钮,点击打开独立元数据窗口。图片信息页不展示后端组装后的生图 Prompt,也不提供复制 Prompt;只展示该图片生成时用户在面板里提交的输入快照,包括普通生成提示词、规范表单字段、角色设定、图标素材描述、快速编辑提示词、重绘提示词,以及角色规范 / 常规参考图 / 图标规范 / 编辑参考图等参考图卡片,并提供“复制信息”复制当前可见字段。参考图输入快照只保存 `refType/refId` 行引用,其中 `refType="project-resource"` 指向 `editor_project_resource.resourceId`,`refType="asset"` 指向 `editor_asset.assetId`;不得把图片 Data URL、普通 URL 或 `objectKey` 写入 `generationInputs.references`。旧数据或上传图片没有输入快照时显示 `-`,禁止回退展示内部 Prompt。 - 生成类产物的工具栏动作 `改造` 不在原产物上原地修改,而是把当次生成输入恢复到对应面板,允许编辑后生成新产物。按钮显隐只按 action capability 判断:确定性派生等不支持改造的 action 隐藏;支持改造的 action 即使参数损坏或必需来源丢失也保留按钮,点击后再显示准确错误,禁止用恢复状态静默隐藏入口。`image.edit` 明确不属于可改造 action:图片快速编辑使用 `targetLayerId` 原位替换画布层,结果层就是被覆盖后的原层,其配方 source 指向替换前的编辑目标,不能再可靠恢复为“改造”新产物;这类结果继续显示“快速编辑”,不显示“改造”。 - V2 恢复严格按顶层 `action`、`fields[].id` 和 `references[].id/refType/refId` 定位,`title` / `label` 只是展示快照,不作为参数键。引用只在当前已 hydrate 的画布图层中按 `refType/refId` 匹配,媒体类型取匹配图层的运行时数据,不重复写入 `generationInputs.references`,也不新增 owner-only 工程资源 / 素材库查询。面板直接上传的参考素材不进入素材库、不是画布图层,因此改造时不恢复;原参考图层已移出画布时同样不恢复。**引用在提交时是否必填,不是恢复时拒绝打开面板的依据**:凡目标面板提供重新选择入口的槽位,无论可选还是必填,缺失时都必须 best effort 打开面板、留空槽位并提示用户重新选择,最终由提交门禁校验;例如角色生成缺少必填角色规范、图标生成缺少必填图标规范、普通图片 / 视频缺少参考图时,都必须打开原 action 面板让用户补选。只有已支持改造但目标面板没有手动替换入口、缺少 source 就无法构造请求时才允许拒绝,当前严格限定为两个例外:`character-animation.generate` 的 source 决定角色动作面板绑定的原角色图层,面板内部不能换角色;`ui-design.extract-assets` 的 source 同时决定框选覆盖层的坐标系,素材提取面板不能改绑另一张设计图。两个例外仍按 action capability 保留改造按钮;source 缺失时点击按钮才显示“无法改造”并分别说明不可替换原因,点击后 source 变化也必须在恢复路径再次校验并拒绝;该失败规则不得用于任何可手动补选的槽位。 -- 改造恢复顺序为:有效 V2;完整的历史生成对话框;按 `assetKind/mediaType`、历史中英文标题别名、资源模型 / 尺寸 / 时长 / `sourceResourceId` 的 legacy adapter,最后缺失值使用当前默认值并显示旧版恢复告警;一旦识别为有效 V2,即使引用缺失也不得降级到 legacy adapter。禁止从中文标题、产物尺寸或 prompt 反推 V2 参数;legacy fallback 除外,但不得回退展示 `actualPrompt` 中的后端拼接 Prompt。当前 BGM 是字段语义上的例外:其新记录中的 `prompt` 与 `actualPrompt` 必须等于输入框已经写回的 canonical `gpt_description_prompt`,不得包含隐藏内容;但改造输入仍只从 `generationInputs.fields` 恢复,不能因此放宽为从资源审计字段回退。 +- 改造恢复顺序为:有效 V2;完整的历史生成对话框;按 `assetKind/mediaType`、历史中英文标题别名、资源模型 / 尺寸 / 时长 / `sourceResourceId` 的 legacy adapter,最后缺失值使用当前默认值并显示“已按旧版数据恢复,部分参数可能使用当前默认值。”。legacy adapter 必须先按 `assetKind/mediaType` 映射角色、图标、UI、场景、视频、音频等原工具;只有图片族且 `assetKind` 为空或未知时才使用默认的普通图片生成工具。无法映射到可用工具时准确提示无法改造。一旦识别为有效 V2,即使引用缺失也不得降级到 legacy adapter。禁止从中文标题、产物尺寸或 prompt 反推 V2 参数;legacy fallback 除外,但不得回退展示 `actualPrompt` 中的后端拼接 Prompt。当前 BGM 是字段语义上的例外:其新记录中的 `prompt` 与 `actualPrompt` 必须等于输入框已经写回的 canonical `gpt_description_prompt`,不得包含隐藏内容;但改造输入仍只从 `generationInputs.fields` 恢复,不能因此放宽为从资源审计字段回退。 - V2 资源 / 素材 hydrate 为前向兼容读取:顶层、`fields[]` 和 `references[]` 出现未来可选属性时只复制当前已知字段,不得让整份配方消失;完美像素操作快照等精确重放路径仍执行递归白名单,并保留历史空字符串的字节级兼容。hydrate 结果必须保留 `absent | valid | invalid-versioned` 运行时来源状态:只有 `absent` 可走 legacy adapter;结构损坏或版本不支持的版本化配方标为 `invalid-versioned`,所有恢复入口都必须拒绝自动降级,并提示“保存的生成参数版本不受支持或数据损坏,无法可靠恢复”。该标记不属于持久化 generationInputs 契约,也不得把原始损坏 JSON 回写。对模型、比例、尺寸、时长、开关等具有明确运行默认值的 V2 参数,缺失与非法值统一补写默认值并告警;允许为空的提示词 / 表单自由文本不因缺失产生参数失效告警。 - 图片生成 / 修改统一经 api-server BFF 接入 VectorEngine。普通生成、生成规范和重绘保留既有 `gpt-image-2` 路径;图片快速编辑统一打开框选区域 + 单提示词 + 模型选择面板,默认沿用原图模型,不展示参考图或比例 / 尺寸控件;其中生成规范类图片固定 `16:9`、`2K`、`gpt-image-2`,面板底部用与可编辑面板一致的比例 / 尺寸 / 模型胶囊按钮展示固定参数,但按钮为禁用态,不允许在该面板改比例、尺寸或模型。`生成角色形象` 与 `生成图标素材` 支持 `nanobanana2`(`gemini-3.1-flash-image-preview`)和 `gpt-image-2`,默认 `nanobanana2`,并在两类面板之间沿用用户上次选择的模型;两类面板不展示抠图背景色或抠图模型选择;前端用户路径固定提交 `screenColor=auto` 和 `segModel=birefnet`,由后端自动决策具体抠图背景色,`anime-seg` 作为内部保留能力不在用户界面暴露。`nanobanana2` 走 `/v1beta/models/{model}:generateContent`,请求体写入 `generationConfig.imageConfig.aspectRatio/imageSize`;`gpt-image-2` 走 `/v1/images/generations` 或 `/v1/images/edits`,请求体按 VectorEngine 文档映射 `size`。宣发素材三个工作流(游戏首图、详情五图、运营海报)固定使用 `gpt-image-2`,面板模型胶囊为禁用态,不提供 `nanobanana2` 入口;前端按 workflow 同时提交 `outputSize`、`aspectRatio` 和 `imageSize`,其中游戏首图为 `720x540 / 4:3`、详情单图为 `720x1280 / 9:16`、运营海报为 `1280x720 / 16:9`;后端收到 `kind: "publication-material"` 时也强制归一为 `gpt-image-2` 生成和计费,生成回填图层优先使用生成占位的 `originalWidth/originalHeight`,即使上游回包尺寸漂移也不得把宣发素材卡片变成随机 `1:1` 或 `4:3`。纯文本生成走 `/api/editor/images/generations`,重绘在前端优先复用当前图层 objectKey;尚未登记的本地图片先上传 OSS,再把 objectKey 交给同一图片生成 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;视频参数按当前正式面板支持的比例、时长、清晰度和声音开关提交,且 Seedance Fast 与 Seedance 标准版必须按各自真实模型 ID 独立映射,不得混用。生成结果以视频图层加入画布。纯文本生成入口采用 Lovart 式画布内占位图 + 锚定生成输入框:点击生成图片后以当前视口世界中心为目标,经统一 placement 避让后创建选中的灰色占位框,输入框跟随占位框显示;普通图片、角色、图标图集、UI 设计图及其重绘 / 改造入口必须在比例或清晰度恢复、切换时同步把占位框 `width/height/originalWidth/originalHeight` 更新为目标像素尺寸,生成中不得继续显示默认 1K 框;UI 素材提取的 1K / 2K 图集占位和旧图片修改入口也分别使用本次目标尺寸与源图真实尺寸。待生成、生成中和失败后保留的占位图都必须继续支持拖动,生成完成时真实生成图或视频落在最新占位框位置,输入框继续跟随新生成图层;占位图失焦时隐藏高亮边框、左上角生成器名称和右上角原始尺寸,重新聚焦时再显示,且名称 / 尺寸在画布缩小时按 viewport 反向缩放保持屏幕尺寸稳定;点击所有图片 / 视频生成入口并确认请求开始后,必须隐藏对应设置面板,只保留画布内占位图或原图预览,并在预览上显示 Lovart 式生成中遮罩,避免“面板仍占屏”或“预览一起消失”。图片快速编辑和重绘在调用图片 BFF 前必须把当前图层图片解析为已上传的 objectKey 或资源 ID;浏览器临时图片需先上传 OSS;视频素材快速编辑走视频生成 BFF,不允许走图片模型;角色动作的 `生成动画` 仍固定使用 `seedance2.0-fast` 动作 / 视频模型,角色动作素材的 `快速编辑` 按当前帧图片走图片编辑。前端不持有 provider 密钥;上游失败或配置缺失时恢复当前生成设置面板展示失败,不创建 mock 成功图。 - 图片画布抠图统一通过唯一、只监听 loopback 的 `bgfilter-worker` 调用 BgFilter provider。手动去除背景面向用户任意图片,仍走登录态同源 BFF `POST /api/editor/images/background-removals` 和外部生成队列;API 在入队前拒绝 `data:` / `blob:` 内联媒体,父流程将稳定引用解析为当前账号已登记且归属已校验的私有 OSS object key,并在同一轮账号项目 / 素材快照读取中同时恢复用户可见源模型,禁止为 object key、所有权和源模型分别重复拉取全量快照;随后只通过一次内部 HTTP RPC 传递 object key、排队预算 `maxQueueWaitMs`、调用预算 `callBudgetMs` 和固定的 `background_mode=complex`、`seg_model=birefnet`、`cross_check=off`,不传源图字节、签名 URL、`file` 或 `screen_color`。子 worker 在每次真实 provider attempt 前签发 600 秒 URL,承担默认 `Q=2048` admission 保险丝、provider 并发 `N=16`、严格最多两次顺序 attempt、响应字节与图片尺寸校验,并把成功图片作为内部 HTTP 二进制 body 直接返回;父流程同步等待该响应且不重试已被 worker 接收的内部 RPC(连接从未建立时按调度方案 §5.1 有界重连)。排队只消耗 `maxQueueWaitMs`,取得 provider permit 后才启动 `callBudgetMs`;attempt 按 `N × est × 2`、调用预算按 `2 × attempt + 1s` 派生,冻结 `est=5000ms` 时分别为 `160s / 321s`。complex 的真实 provider 失败会累计并打开自身熔断,但与 flat 状态隔离;complex 任意失败或熔断仍直接返回父流程失败,不接入阿里云 / 本地键色降级。provider 配置继续统一使用 `GENARRATIVE_EDITOR_BGFILTER_BASE_URL` 和 `GENARRATIVE_EDITOR_BGFILTER_TOKEN`,父子共同使用 `GENARRATIVE_BGFILTER_WORKER_CONCURRENCY` 与 `GENARRATIVE_EDITOR_BGFILTER_SINGLE_IMAGE_ESTIMATE_MS` 派生预算;旧 `GENARRATIVE_EDITOR_BACKGROUND_REMOVAL_TOKEN` 只作为 provider token 兼容别名;内部调用另使用 `GENARRATIVE_BGFILTER_WORKER_BASE_URL` 和独立内部 Token。所有令牌只在服务端注入,前端不持有令牌。成功字节返回父流程后,仍由父流程完成最终处理、OSS / asset object 持久化、结果图层与最新项目快照写回;接口只向前端返回 `queueState`,有项目上下文时前端同时创建去背景生成占位并把 `canvasCompletion` 交给后端。 diff --git a/src/components/image-editor/ImageCanvasEditorTypes.ts b/src/components/image-editor/ImageCanvasEditorTypes.ts index 2ef85dda5..56f3937a4 100644 --- a/src/components/image-editor/ImageCanvasEditorTypes.ts +++ b/src/components/image-editor/ImageCanvasEditorTypes.ts @@ -130,7 +130,10 @@ export type CanvasLayer = { id: string; resourceId: string; resourcePersistenceState?: - 'pending' | 'registered' | 'self-contained-local' | 'unresolved-local'; + | 'pending' + | 'registered' + | 'self-contained-local' + | 'unresolved-local'; title: string; src: string; mediaType?: CanvasMediaType; @@ -263,7 +266,6 @@ export type GenerateDialogState = { assetLabel?: string; status: 'idle' | 'generating' | 'pending-confirmation' | 'failed'; composerOpen?: boolean; - isRedrawAsNewArtifact?: boolean; sourceLayerId?: string; generatedLayerId?: string; specType?: SpecGenerationType; @@ -442,7 +444,6 @@ export type CanvasContextMenuState = export type QuickEditPanelState = { referenceUploadContextId?: string; - mode?: 'quick-edit' | 'redraw'; sourceLayerId: string; prompt: string; assetLabel?: string; @@ -469,7 +470,12 @@ export type CropExpandPanelState = { }; export type CropExpandRatioValue = - 'free' | '1:1' | '4:3' | '3:4' | '16:9' | '9:16'; + | 'free' + | '1:1' + | '4:3' + | '3:4' + | '16:9' + | '9:16'; export type CropExpandResizeHandle = | 'left' diff --git a/src/components/image-editor/ImageCanvasEditorView.tsx b/src/components/image-editor/ImageCanvasEditorView.tsx index 288ceca5e..3ff601c19 100644 --- a/src/components/image-editor/ImageCanvasEditorView.tsx +++ b/src/components/image-editor/ImageCanvasEditorView.tsx @@ -1169,8 +1169,9 @@ export function ImageCanvasEditorView({ imageSequenceDurationMs: asset.imageSequenceDurationMs ?? undefined, assetKind: canvasAssetKindOrNull(asset.assetKind), generationInputs: generationInputsOrNull(asset.generationInputs), - generationInputsHydrationState: - hydrateCanvasGenerationInputsResult(asset.generationInputs).state, + generationInputsHydrationState: hydrateCanvasGenerationInputsResult( + asset.generationInputs, + ).state, }, ]); setAssetFolders((currentFolders) => @@ -1750,7 +1751,7 @@ export function ImageCanvasEditorView({ : null; } if (target === 'quick-edit-reference') { - return quickEditPanel && quickEditPanel.mode !== 'redraw' + return quickEditPanel ? { label: '快速编辑参考图', currentCount: imageReferenceCount( diff --git a/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts b/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts index 235b3f282..7782a2ba6 100644 --- a/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts @@ -21,10 +21,10 @@ import { createGenerateDialogDraft, createIconGenerationDialogDraft, createLayerGenerationDialogDraft, + createLegacyImageRedrawGenerationDialogDraft, createPublicationGenerationDialogDraft, createQuickEditGenerationDialogDraft, createQuickEditPanelDraft, - createRedrawPanelDraft, createSameSourceGenerationDialogDraft, createSceneGenerationDialogDraft, createSoundEffectGenerationDialogDraft, @@ -469,7 +469,6 @@ describe('ImageCanvasGenerationDialogModel', () => { imageSize: '1K', }); expect(createQuickEditPanelDraft(sourceLayer)).toEqual({ - mode: 'quick-edit', sourceLayerId: 'layer-source', prompt: '', size: '1024x768', @@ -479,7 +478,13 @@ describe('ImageCanvasGenerationDialogModel', () => { quickEditReferences: [], status: 'idle', }); - expect(createRedrawPanelDraft(sourceLayer).prompt).toBe('原图提示'); + expect( + createLegacyImageRedrawGenerationDialogDraft({ + sourceLayer, + canvasSize: { width: 960, height: 720 }, + viewport: { x: 0, y: 0, scale: 1 }, + }), + ).toMatchObject({ mode: 'generate', prompt: '原图提示' }); expect(createCharacterAnimationPanelDraft(sourceLayer)).toEqual({ sourceLayerId: 'layer-source', promptText: '', @@ -590,21 +595,23 @@ describe('ImageCanvasGenerationDialogModel', () => { for (const testCase of cases) { expect( - createRedrawPanelDraft( - createLayer({ + createLegacyImageRedrawGenerationDialogDraft({ + sourceLayer: createLayer({ prompt: '【系统内置】完整拼接提示词,包含默认规则和模型约束。', generationInputs: { fields: testCase.fields, references: [], }, }), - ).prompt, + canvasSize: { width: 960, height: 720 }, + viewport: { x: 0, y: 0, scale: 1 }, + }).prompt, ).toBe(testCase.expectedPrompt); } expect( - createRedrawPanelDraft( - createLayer({ + createLegacyImageRedrawGenerationDialogDraft({ + sourceLayer: createLayer({ prompt: '【系统内置】固定素材提取提示词', generationInputs: { fields: [ @@ -618,7 +625,9 @@ describe('ImageCanvasGenerationDialogModel', () => { references: [], }, }), - ).prompt, + canvasSize: { width: 960, height: 720 }, + viewport: { x: 0, y: 0, scale: 1 }, + }).prompt, ).toBe(''); }); @@ -1269,7 +1278,7 @@ describe('ImageCanvasGenerationDialogModel', () => { }); }); - it('restores quick-edit references and the persisted image model', () => { + it('does not restore in-place image edits as remodel generators', () => { const editSource = createLayer({ id: 'layer-edit-source', resourceId: 'resource-edit-source', @@ -1279,55 +1288,44 @@ describe('ImageCanvasGenerationDialogModel', () => { resourceId: 'resource-style-reference', title: '风格参考', }); + const editResult = createLayer({ + id: 'layer-edit-result', + resourceId: 'resource-edit-result', + sourceType: 'generated', + generationInputs: { + version: 2, + action: 'image.edit', + fields: [ + { id: 'prompt', title: '提示词', value: '改成雨天' }, + { id: 'model', title: '模型', value: IMAGE_MODEL_NANOBANANA2 }, + { id: 'aspectRatio', title: '比例', value: '16:9' }, + { id: 'imageSize', title: '尺寸', value: '2K' }, + ], + references: [ + { + id: 'source', + title: '原图', + refType: 'project-resource', + refId: 'resource-edit-source', + }, + { + id: 'reference', + title: '参考图 1', + refType: 'project-resource', + refId: 'resource-style-reference', + }, + ], + }, + }); expect( createLayerGenerationDialogDraft({ - sourceLayer: createLayer({ - id: 'layer-edit-result', - resourceId: 'resource-edit-result', - sourceType: 'generated', - generationInputs: { - version: 2, - action: 'image.edit', - fields: [ - { id: 'prompt', title: '提示词', value: '改成雨天' }, - { id: 'model', title: '模型', value: IMAGE_MODEL_NANOBANANA2 }, - { id: 'aspectRatio', title: '比例', value: '16:9' }, - { id: 'imageSize', title: '尺寸', value: '2K' }, - ], - references: [ - { - id: 'source', - title: '原图', - refType: 'project-resource', - refId: 'resource-edit-source', - }, - { - id: 'reference', - title: '参考图 1', - refType: 'project-resource', - refId: 'resource-style-reference', - }, - ], - }, - }), + sourceLayer: editResult, availableLayers: [editSource, styleReference], canvasSize: { width: 960, height: 720 }, viewport: { x: 0, y: 0, scale: 1 }, }), - ).toMatchObject({ - mode: 'quick-edit', - prompt: '改成雨天', - imageModel: IMAGE_MODEL_NANOBANANA2, - aspectRatio: '16:9', - imageSize: '2K', - generationReferences: [ - expect.objectContaining({ - id: 'canvas-layer-style-reference', - label: '风格参考', - }), - ], - }); + ).toBeNull(); }); it('uses the V2 source reference ahead of a conflicting legacy animation source', () => { diff --git a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts index b13778570..9c2bcf69e 100644 --- a/src/components/image-editor/ImageCanvasGenerationDialogModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationDialogModel.ts @@ -1315,40 +1315,6 @@ function createNormalizedGenerationDialogDraft({ : null; } - if (inputs.action === 'image.edit') { - const source = resolveNormalizedReferences( - sourceLayer, - 'source', - availableLayers, - )[0]; - const sourceLayerForEdit = source - ? (availableLayers ?? []).find( - (layer) => createCanvasLayerReference(layer).id === source.id, - ) - : undefined; - if (!sourceLayerForEdit) { - return null; - } - const size = resolveEditorImageGenerationPixelSize({ - model: getNormalizedString(sourceLayer, 'model', DEFAULT_IMAGE_MODEL), - aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '1:1'), - imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'), - }); - return { - ...createQuickEditGenerationDialogDraft({ - sourceLayer: sourceLayerForEdit, - prompt, - references, - model: getNormalizedString(sourceLayer, 'model', DEFAULT_IMAGE_MODEL), - aspectRatio: getNormalizedString(sourceLayer, 'aspectRatio', '1:1'), - imageSize: getNormalizedString(sourceLayer, 'imageSize', '1K'), - frame: size, - }), - composerOpen: true, - isRedrawAsNewArtifact: true, - }; - } - return null; } @@ -1956,7 +1922,6 @@ export function createQuickEditPanelDraft( dimensionOptions.imageSizes[0] ?? '1K'); return { - mode: 'quick-edit', sourceLayerId: sourceLayer.id, prompt: '', size: formatImageSizeValue( @@ -2021,20 +1986,29 @@ export function createQuickEditGenerationDialogDraft({ }; } -export function createRedrawPanelDraft( - sourceLayer: CanvasLayer, - options: { - imageModel?: string; - aspectRatio?: string; - imageSize?: string; - } = {}, -): QuickEditPanelState { - return { - ...createQuickEditPanelDraft(sourceLayer, options), - mode: 'redraw', - model: normalizeEditorImageModel(options.imageModel ?? sourceLayer.model), - prompt: resolveUserGenerationPromptSnapshot(sourceLayer), - }; +export function createLegacyImageRedrawGenerationDialogDraft({ + sourceLayer, + canvasSize, + viewport, + sourceDialog, +}: { + sourceLayer: CanvasLayer; + canvasSize: CanvasSize; + viewport: CanvasViewport; + sourceDialog?: CanvasGenerationDialogState | null; +}): Omit { + return placeDraftBesideSourceLayer( + restoreSharedImageOptions( + { + ...createGenerateDialogDraft({ canvasSize, viewport }), + prompt: resolveUserGenerationPromptSnapshot(sourceLayer), + sourceLayerId: sourceLayer.id, + }, + sourceLayer, + sourceDialog, + ), + sourceLayer, + ); } export function createCharacterAnimationPanelDraft( @@ -2261,11 +2235,7 @@ export function appendQuickEditReference( panel: QuickEditPanelState | null, layer: CanvasLayer, ): QuickEditPanelState | null { - if ( - !panel || - panel.mode === 'redraw' || - getReferenceMediaType(layer) !== 'image' - ) { + if (!panel || getReferenceMediaType(layer) !== 'image') { return panel; } return { diff --git a/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts b/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts index 10e8464cd..9d5e8a4ef 100644 --- a/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts +++ b/src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts @@ -10,7 +10,6 @@ import { createCharacterAnimationResultLayer, createGeneratedResultLayer, createIconSpritesheetResultLayers, - createQuickEditResultLayer, createVideoResultLayer, } from './ImageCanvasGenerationLayerModel'; @@ -232,57 +231,6 @@ describe('ImageCanvasGenerationLayerModel', () => { }); }); - it('creates quick edit layers centered on the generating placeholder', () => { - const sourceLayer = createSourceLayer(); - const layer = createQuickEditResultLayer({ - generated: createGenerated({ width: 1536, height: 1024 }), - generatedIndex: 10, - sourceLayer, - generationInputs: createGenerationInputs(), - frame: { - x: 600, - y: 300, - width: 320, - height: 240, - originalWidth: 1536, - originalHeight: 1024, - }, - }); - - expect(layer).toMatchObject({ - id: 'layer-quick-edit-10', - x: -8, - y: -92, - width: 1536, - height: 1024, - }); - }); - - it('creates quick edit layers with source group and asset kind preserved', () => { - const sourceLayer = createSourceLayer(); - const layer = createQuickEditResultLayer({ - generated: createGenerated({ width: 1536, height: 1024 }), - generatedIndex: 9, - sourceLayer, - generationInputs: createGenerationInputs(), - }); - - expect(layer).toMatchObject({ - id: 'layer-quick-edit-9', - resourceId: 'local-resource-quick-edit-9', - title: '源图 快速编辑', - x: 472, - y: 140, - width: 1536, - height: 1024, - originalWidth: 1536, - originalHeight: 1024, - sourceResourceId: 'resource-source', - groupId: 'group-a', - assetKind: 'spec', - }); - }); - it('applies quick edits to the source layer in place', () => { const sourceLayer = createSourceLayer({ width: 320, diff --git a/src/components/image-editor/ImageCanvasGenerationLayerModel.ts b/src/components/image-editor/ImageCanvasGenerationLayerModel.ts index ee0806117..5dc1e1a31 100644 --- a/src/components/image-editor/ImageCanvasGenerationLayerModel.ts +++ b/src/components/image-editor/ImageCanvasGenerationLayerModel.ts @@ -32,16 +32,6 @@ type GeneratedResultLayerOptions = { generationInputs?: CanvasGenerationInputs; }; -type QuickEditResultLayerOptions = { - generated: EditorImageGenerationResult; - generatedIndex: number; - sourceLayer: CanvasLayer; - generationInputs: CanvasGenerationInputs; - mode?: 'quick-edit' | 'redraw'; - frame?: GenerateDialogState['placeholder']; - title?: string; -}; - type IconSpritesheetResultLayerOptions = { generated: EditorIconSpritesheetGenerationResult; iconResults?: EditorIconSpritesheetIconResult[] | null; @@ -183,67 +173,6 @@ export function createGeneratedResultLayer({ ); } -export function createQuickEditResultLayer({ - generated, - generatedIndex, - sourceLayer, - generationInputs, - mode = 'quick-edit', - frame, - title, -}: QuickEditResultLayerOptions): CanvasLayer { - const originalWidth = - frame?.originalWidth || - generated.width || - sourceLayer.originalWidth || - 1024; - const originalHeight = - frame?.originalHeight || - generated.height || - sourceLayer.originalHeight || - 1024; - const { width, height } = resolveLayerResolutionSize( - originalWidth, - originalHeight, - { - width: sourceLayer.width, - height: sourceLayer.height, - }, - ); - const frameX = - frame && frame.width > 0 - ? frame.x + frame.width / 2 - width / 2 - : undefined; - const frameY = - frame && frame.height > 0 - ? frame.y + frame.height / 2 - height / 2 - : undefined; - - return applyGeneratedMetadata( - { - id: `layer-quick-edit-${generatedIndex}`, - resourceId: `local-resource-quick-edit-${generatedIndex}`, - title: - title ?? - `${sourceLayer.title} ${mode === 'redraw' ? '重绘' : '快速编辑'}`, - src: generated.imageSrc, - x: frameX ?? sourceLayer.x + sourceLayer.width + 32, - y: frameY ?? sourceLayer.y, - width, - height, - originalWidth, - originalHeight, - zIndex: generatedIndex + 10, - sourceType: generated.sourceType, - sourceResourceId: sourceLayer.resourceId, - groupId: sourceLayer.groupId, - assetKind: sourceLayer.assetKind, - }, - generated, - generationInputs, - ); -} - export function applyImageEditResultToSourceLayer({ generated, sourceLayer, diff --git a/src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx b/src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx index 211c9f4c3..a7d9e6b28 100644 --- a/src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx +++ b/src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx @@ -100,10 +100,7 @@ describe('ImageCanvasQuickEditPanelView', () => { />, ); - setPlainTextEditorValue( - screen.getByLabelText('快速编辑提示词'), - '新提示', - ); + setPlainTextEditorValue(screen.getByLabelText('快速编辑提示词'), '新提示'); expect(screen.getByLabelText('当前提示词').textContent).toBe('新提示'); expect(screen.getByLabelText('当前尺寸').textContent).toBe('1024x1024'); @@ -141,9 +138,7 @@ describe('ImageCanvasQuickEditPanelView', () => { />, ); - expect(screen.getByRole('alert').textContent).toContain( - '图集快速编辑失败', - ); + expect(screen.getByRole('alert').textContent).toContain('图集快速编辑失败'); }); it('submits without rendering a standalone close button', () => { @@ -171,27 +166,6 @@ describe('ImageCanvasQuickEditPanelView', () => { ).toBeNull(); }); - it('shows configured mud point price inside redraw submit button', () => { - render( - , - ); - - expect(screen.getByRole('button', { name: '生成' }).textContent).toBe( - '生成3泥点', - ); - }); - it('hides quick edit references from the simplified panel', () => { render( diff --git a/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx b/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx index 7561bcfd3..3172f3999 100644 --- a/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx +++ b/src/components/image-editor/ImageCanvasSelectedLayerToolbarView.test.tsx @@ -98,9 +98,7 @@ describe('ImageCanvasSelectedLayerToolbarView', () => { ); expect(props.onRemoveBackground).toHaveBeenCalledWith(props.selectedLayer); expect(props.onPerfectPixel).toHaveBeenCalledWith(props.selectedLayer); - expect(props.onOpenRedrawPanel).toHaveBeenCalledWith( - props.selectedLayer, - ); + expect(props.onOpenRedrawPanel).toHaveBeenCalledWith(props.selectedLayer); expect(props.onDownloadLayer).toHaveBeenCalledWith(props.selectedLayer); expect( within(toolbar).queryByRole('button', { name: '删除图片' }), @@ -391,7 +389,7 @@ describe('ImageCanvasSelectedLayerToolbarView', () => { expect(screen.queryByRole('button', { name: '改造' })).toBeNull(); }); - it('hides redraw for in-place quick-edit results', () => { + it('keeps quick edit but hides redraw for in-place quick-edit results', () => { renderSelectedToolbar({ selectedLayer: createLayer({ generationInputs: { diff --git a/src/components/image-editor/ImageCanvasUploadModel.ts b/src/components/image-editor/ImageCanvasUploadModel.ts index 80c2730b7..02077fd62 100644 --- a/src/components/image-editor/ImageCanvasUploadModel.ts +++ b/src/components/image-editor/ImageCanvasUploadModel.ts @@ -285,7 +285,7 @@ export function applyQuickEditReferenceUpload({ panel: QuickEditPanelState | null; references: CharacterReferenceImage[]; }): QuickEditPanelState | null { - if (!panel || panel.mode === 'redraw') { + if (!panel) { return panel; } return { diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx index fb1e8ec49..95f2c5ab5 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx @@ -901,7 +901,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { render( { render( { { { render( { expect(refreshWalletBalance).toHaveBeenCalledTimes(1); }); - it('submits character redraws through the character image prompt contract', async () => { - generateEditorImageMock.mockResolvedValueOnce( - createGenerated({ - prompt: '角色换成蓝色披风', - warning: { - code: 'postprocess-failed-source-preserved', - reason: '生成任务成功,后处理失败。', - }, - }), - ); - render( - , - ); - - fireEvent.click(screen.getByRole('button', { name: '提交快速编辑' })); - - await waitFor(() => { - expect(generateEditorImageMock).toHaveBeenCalledWith( - expect.objectContaining({ - prompt: '角色换成蓝色披风', - size: '1024x768', - aspectRatio: '4:3', - imageSize: '1K', - kind: 'character', - model: 'gpt-image-2', - referenceImageSrcs: ['resource-source'], - assetKind: 'character', - assetLabel: '蓝披风勇者', - canvasCompletion: expect.objectContaining({ - title: '蓝披风勇者', - }), - }), - ); - }); - 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:角色换成蓝色披风:-', - ); - expect(screen.getByTestId('generation-warning').textContent).toBe( - '生成任务成功,后处理失败。', - ); - }); - it('submits video quick edits through the video API with the source video', async () => { const applyProjectSnapshot = vi.fn(); const upsertGeneratedAsset = vi.fn(); @@ -1523,7 +1454,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { }), ]} initialQuickEditPanel={{ - mode: 'quick-edit', sourceLayerId: 'layer-source', prompt: '把当前图改成雨天', size: '854x480', @@ -1683,7 +1613,6 @@ describe('useImageCanvasGenerationSubmissionWorkflow', () => { }), ]} initialQuickEditPanel={{ - mode: 'quick-edit', sourceLayerId: 'layer-source', prompt: '让当前图跳得更高', size: '1024x1024', diff --git a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts index 526fe16ce..38fbc9b95 100644 --- a/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts @@ -57,12 +57,10 @@ import { createCharacterAnimationResultLayer, createGeneratedResultLayer, createIconSpritesheetResultLayers, - createQuickEditResultLayer, createVideoResultLayer, } from './ImageCanvasGenerationLayerModel'; import { buildCharacterAnimationGenerationInputs, - buildEditGenerationInputs, buildQuickEditGenerationInputs, buildUiDesignAssetExtractionGenerationInputs, buildVideoGenerationInputs, @@ -872,81 +870,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({ ], ); - const addQuickEditResultLayer = useCallback( - ( - generated: Parameters[0]['generated'], - sourceLayer: CanvasLayer, - generationInputs: CanvasGenerationInputs, - mode: QuickEditPanelState['mode'] = 'quick-edit', - options: { - frame?: GenerateDialogState['placeholder']; - dialogId?: string; - title?: string; - } = {}, - ) => { - if ( - options.dialogId && - !hasCanvasGenerationDialogById(options.dialogId) - ) { - return; - } - layerCounterRef.current += 1; - const generatedIndex = layerCounterRef.current; - const nextLayer = createQuickEditResultLayer({ - generated, - generatedIndex, - sourceLayer, - generationInputs, - mode, - frame: options.frame, - title: options.title, - }); - - addGeneratedLayersToCanvas([nextLayer]); - selectSingleLayer(mode === 'redraw' ? sourceLayer.id : nextLayer.id); - setActiveSidebarPanel('layers'); - if (mode === 'redraw') { - setQuickEditPanel((currentPanel) => - currentPanel?.sourceLayerId === sourceLayer.id - ? { - ...currentPanel, - mode: 'redraw', - status: 'idle', - errorMessage: undefined, - } - : currentPanel, - ); - } else { - setQuickEditPanel(null); - setActiveTool('select'); - if (options.dialogId) { - updateCanvasGenerationDialogById( - options.dialogId, - (currentDialog) => ({ - ...currentDialog, - status: 'idle', - composerOpen: false, - generatedLayerId: nextLayer.id, - errorMessage: undefined, - }), - ); - } - } - fitLayers([sourceLayer, nextLayer], { captureHistory: false }); - }, - [ - addGeneratedLayersToCanvas, - fitLayers, - layerCounterRef, - selectSingleLayer, - setActiveSidebarPanel, - setActiveTool, - setQuickEditPanel, - updateCanvasGenerationDialogById, - hasCanvasGenerationDialogById, - ], - ); - const applyQuickEditResultToSourceLayer = useCallback( ( generated: Parameters< @@ -1507,15 +1430,11 @@ export function useImageCanvasGenerationSubmissionWorkflow({ return; } - const panelMode = quickEditPanel.mode ?? 'quick-edit'; const quickEditResultTitle = resolveGenerationAssetLabel( quickEditPanel.assetLabel, - `${quickEditSourceLayer.title} ${panelMode === 'redraw' ? '重绘' : '快速编辑'}`, + `${quickEditSourceLayer.title} 快速编辑`, ); - const quickEditReferences = - panelMode === 'redraw' || panelMode === 'quick-edit' - ? [] - : (quickEditPanel.quickEditReferences ?? []); + const quickEditReferences: CharacterReferenceImage[] = []; const quickEditAspectRatio = quickEditPanel.aspectRatio ?? inferEditorImageAspectRatio( @@ -1542,13 +1461,9 @@ export function useImageCanvasGenerationSubmissionWorkflow({ imageSize: quickEditImageSize, }); const quickEditSize = `${quickEditOutputSize.width}x${quickEditOutputSize.height}`; - const basePrompt = - quickEditPanel.prompt.trim() || - (panelMode === 'redraw' ? '重绘图片' : '快速编辑图片'); - const normalizedPrompt = panelMode === 'redraw' ? basePrompt : basePrompt; + const normalizedPrompt = quickEditPanel.prompt.trim() || '快速编辑图片'; let quickEditDialogId: string | undefined; const shouldUseCanvasPlaceholder = - panelMode === 'quick-edit' && isVideoQuickEditSource(quickEditSourceLayer); if (shouldUseCanvasPlaceholder) { const quickEditDraft = createQuickEditGenerationDialogDraft({ @@ -1594,21 +1509,11 @@ export function useImageCanvasGenerationSubmissionWorkflow({ } try { - const needsExtraReferenceImageSrcs = - panelMode === 'quick-edit' || - panelMode === 'redraw' || - isVideoQuickEditSource(quickEditSourceLayer); - const extraReferenceImageSrcs = needsExtraReferenceImageSrcs - ? await Promise.all( - quickEditReferences.map((reference) => - resolveEditorGenerationMediaReference( - reference, - 'image', - projectId, - ), - ), - ) - : []; + const extraReferenceImageSrcs = await Promise.all( + quickEditReferences.map((reference) => + resolveEditorGenerationMediaReference(reference, 'image', projectId), + ), + ); const quickEditFrame = quickEditDialogId ? getGeneratingDialogPlaceholder({ id: quickEditDialogId, @@ -1625,10 +1530,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({ originalWidth: quickEditOutputSize.width, originalHeight: quickEditOutputSize.height, }); - if ( - panelMode === 'quick-edit' && - isVideoQuickEditSource(quickEditSourceLayer) - ) { + if (isVideoQuickEditSource(quickEditSourceLayer)) { const videoModel = DEFAULT_VIDEO_MODEL; const videoResolution = '480p' as const; const videoDurationSeconds = 4 as const; @@ -1712,7 +1614,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({ return; } const quickEditSelectionMarks = - panelMode === 'quick-edit' && quickEditSelectionState?.sourceLayerId === quickEditSourceLayer.id ? quickEditSelectionState.marks : []; @@ -1738,85 +1639,38 @@ export function useImageCanvasGenerationSubmissionWorkflow({ projectId, ); } - const generationInputs = - panelMode === 'redraw' - ? buildEditGenerationInputs( - '重绘提示词', - normalizedPrompt, - quickEditSourceLayer, - { - model: normalizedQuickEditModel, - aspectRatio: quickEditAspectRatio, - imageSize: quickEditImageSize, - }, - ) - : buildQuickEditGenerationInputs( - '快速编辑提示词', - normalizedPrompt, - quickEditSourceLayer, - quickEditReferences, - { - model: normalizedQuickEditModel, - aspectRatio: quickEditAspectRatio, - imageSize: quickEditImageSize, - }, - ); - const isCharacterRedraw = - panelMode === 'redraw' && - quickEditSourceLayer.assetKind === 'character'; - const generated = - panelMode === 'quick-edit' - ? await runEditorGenerationWithWalletRefresh( - editEditorImage({ - prompt: normalizedPrompt, - sourceImageSrc: sourceReferenceImageSrc, - size: quickEditSize, - model: normalizedQuickEditModel, - aspectRatio: quickEditAspectRatio, - imageSize: quickEditImageSize, - ...(extraReferenceImageSrcs.length - ? { referenceImageSrcs: extraReferenceImageSrcs } - : {}), - projectId, - assetKind: quickEditSourceLayer.assetKind, - generationInputs, - assetFolderId, - assetLabel: quickEditResultTitle, - sourceResourceId: quickEditSourceLayer.resourceId, - targetLayerId: quickEditSourceLayer.id, - }), - onWalletBalanceMayHaveChanged, - ) - : await runEditorGenerationWithWalletRefresh( - generateEditorImage({ - prompt: normalizedPrompt, - size: quickEditSize, - aspectRatio: quickEditAspectRatio, - imageSize: quickEditImageSize, - kind: isCharacterRedraw ? 'character' : 'quick-edit', - model: normalizedQuickEditModel, - referenceImageSrcs: [ - ...extraReferenceImageSrcs, - sourceReferenceImageSrc, - ], - projectId, - assetKind: quickEditSourceLayer.assetKind, - generationInputs, - assetFolderId, - assetLabel: quickEditResultTitle, - sourceResourceId: quickEditSourceLayer.resourceId, - ...(projectId && quickEditCanvasCompletionPlaceholder - ? { - canvasCompletion: { - dialogId: quickEditDialogId, - title: quickEditResultTitle, - placeholder: quickEditCanvasCompletionPlaceholder, - }, - } - : {}), - }), - onWalletBalanceMayHaveChanged, - ); + const generationInputs = buildQuickEditGenerationInputs( + '快速编辑提示词', + normalizedPrompt, + quickEditSourceLayer, + quickEditReferences, + { + model: normalizedQuickEditModel, + aspectRatio: quickEditAspectRatio, + imageSize: quickEditImageSize, + }, + ); + const generated = await runEditorGenerationWithWalletRefresh( + editEditorImage({ + prompt: normalizedPrompt, + sourceImageSrc: sourceReferenceImageSrc, + size: quickEditSize, + model: normalizedQuickEditModel, + aspectRatio: quickEditAspectRatio, + imageSize: quickEditImageSize, + ...(extraReferenceImageSrcs.length + ? { referenceImageSrcs: extraReferenceImageSrcs } + : {}), + projectId, + assetKind: quickEditSourceLayer.assetKind, + generationInputs, + assetFolderId, + assetLabel: quickEditResultTitle, + sourceResourceId: quickEditSourceLayer.resourceId, + targetLayerId: quickEditSourceLayer.id, + }), + onWalletBalanceMayHaveChanged, + ); notifyEditorGenerationWarning( generated.warning?.reason, onGenerationWarning, @@ -1831,45 +1685,25 @@ export function useImageCanvasGenerationSubmissionWorkflow({ onGenerationWarning, ) ) { - if (panelMode === 'quick-edit') { - setQuickEditPanel(null); - setActiveTool('select'); - setActiveSidebarPanel('layers'); - } + setQuickEditPanel(null); + setActiveTool('select'); + setActiveSidebarPanel('layers'); return; } if (generated.project && applyProjectSnapshot) { applyProjectSnapshot(generated.project); - if (panelMode === 'quick-edit') { - setQuickEditPanel(null); - setActiveTool('select'); - setActiveSidebarPanel('layers'); - } + setQuickEditPanel(null); + setActiveTool('select'); + setActiveSidebarPanel('layers'); return; } - if (panelMode === 'quick-edit') { - applyQuickEditResultToSourceLayer( - generated, - quickEditSourceLayer, - generationInputs, - quickEditDialogId, - quickEditResultTitle, - ); - } else { - addQuickEditResultLayer( - generated, - quickEditSourceLayer, - generationInputs, - panelMode, - quickEditDialogId - ? { - frame: quickEditFrame, - dialogId: quickEditDialogId, - title: quickEditResultTitle, - } - : { title: quickEditResultTitle }, - ); - } + applyQuickEditResultToSourceLayer( + generated, + quickEditSourceLayer, + generationInputs, + quickEditDialogId, + quickEditResultTitle, + ); } catch (error) { if (quickEditDialogId) { updateCanvasGenerationDialogById( @@ -1894,7 +1728,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({ }); } }, [ - addQuickEditResultLayer, addVideoResultLayer, applyQuickEditResultToSourceLayer, applyProjectSnapshot, @@ -2121,7 +1954,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({ generationInputs: submissionPlan.generationInputs, }); } else if (submissionPlan.kind === 'quick-edit') { - const isRedrawAsNewArtifact = Boolean(dialog.isRedrawAsNewArtifact); const quickEditSelectionMarks = quickEditSelectionState?.sourceLayerId === submissionPlan.sourceLayer.id @@ -2166,11 +1998,6 @@ export function useImageCanvasGenerationSubmissionWorkflow({ ), ), ); - const quickEditPlaceholderSize = - getCanvasCompletionPlaceholderSizeFromPlan({ - sourceLayer: submissionPlan.sourceLayer, - outputSize: submissionPlan.editInput.size, - }); const generated = await runEditorGenerationWithWalletRefresh( editEditorImage({ prompt: submissionPlan.normalizedPrompt, @@ -2185,25 +2012,7 @@ export function useImageCanvasGenerationSubmissionWorkflow({ assetFolderId, assetLabel: submissionPlan.result.title, sourceResourceId: submissionPlan.sourceLayer.resourceId, - ...(isRedrawAsNewArtifact - ? { - canvasCompletion: { - dialogId: canvasDialog?.id, - title: submissionPlan.result.title, - placeholder: - getGeneratingDialogPlaceholder(dialog) ?? - buildRightSideCanvasCompletionPlaceholder( - submissionPlan.sourceLayer, - { - width: quickEditPlaceholderSize.width, - height: quickEditPlaceholderSize.height, - originalWidth: quickEditPlaceholderSize.width, - originalHeight: quickEditPlaceholderSize.height, - }, - ), - }, - } - : { targetLayerId: submissionPlan.sourceLayer.id }), + targetLayerId: submissionPlan.sourceLayer.id, }), onWalletBalanceMayHaveChanged, ); @@ -2225,21 +2034,13 @@ export function useImageCanvasGenerationSubmissionWorkflow({ } return; } - if (isRedrawAsNewArtifact) { - addGeneratedResultLayer(generated, { - sourceLayer: submissionPlan.sourceLayer, - title: submissionPlan.result.title, - generationInputs: submissionPlan.result.generationInputs, - }); - } else { - applyQuickEditResultToSourceLayer( - generated, - submissionPlan.sourceLayer, - submissionPlan.result.generationInputs, - canvasDialog?.id, - submissionPlan.result.title, - ); - } + applyQuickEditResultToSourceLayer( + generated, + submissionPlan.sourceLayer, + submissionPlan.result.generationInputs, + canvasDialog?.id, + submissionPlan.result.title, + ); } else if (submissionPlan.kind === 'icon-spec') { const iconSpecInput = submissionPlan.input; const canvasCompletionPlaceholder = diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx b/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx index 339c46c7d..60bda118e 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.test.tsx @@ -563,7 +563,7 @@ function GenerationWorkflowHarness({ {workflow.quickEditPanel - ? `${workflow.quickEditPanel.mode ?? 'quick-edit'}:${workflow.quickEditPanel.model}:${workflow.quickEditPanel.aspectRatio ?? '-'}:${workflow.quickEditPanel.imageSize ?? '-'}` + ? `quick-edit:${workflow.quickEditPanel.model}:${workflow.quickEditPanel.aspectRatio ?? '-'}:${workflow.quickEditPanel.imageSize ?? '-'}` : '-'} @@ -1563,14 +1563,14 @@ describe('useImageCanvasGenerationWorkflow', () => { ); }); - it('opens generated layer remodel with user input snapshot instead of internal prompt', () => { + it('opens legacy image remodel with user input snapshot in the default image tool', () => { render(); fireEvent.click(screen.getByRole('button', { name: '打开用户快照改造' })); expect(screen.getByTestId('quick-edit').textContent).toBe('-'); expect(screen.getByTestId('dialog').textContent).toBe( - 'quick-edit:idle:open:-:placeholder', + 'generate:idle:open:-:placeholder', ); expect(screen.getByTestId('dialog-prompt').textContent).toBe( '用户的视频描述', @@ -1592,19 +1592,59 @@ describe('useImageCanvasGenerationWorkflow', () => { ); }); - it('opens image redraw as a new canvas generator next to the source layer', () => { + it('opens image remodel without an origin action in the default image tool', () => { render(); fireEvent.click(screen.getByRole('button', { name: '打开生成' })); fireEvent.click(screen.getByRole('button', { name: '打开图片改造' })); expect(screen.getByTestId('dialog').textContent).toBe( - 'quick-edit:idle:open:-:placeholder', + 'generate:idle:open:-:placeholder', ); expect(screen.getByTestId('quick-edit').textContent).toBe('-'); expect(screen.getByTestId('placeholder').textContent).toContain('1024:768'); expect(screen.getByTestId('generation-dialogs').textContent).toBe( - 'generation-dialog-1:generate:closed|generation-dialog-2:quick-edit:open', + 'generation-dialog-1:generate:closed|generation-dialog-2:generate:open', + ); + }); + + it('rejects image.edit remodel even when the internal callback is invoked', () => { + render( + , + ); + + fireEvent.click(screen.getByRole('button', { name: '打开图片改造' })); + + expect(screen.getByTestId('dialog').textContent).toBe('-'); + expect(screen.getByTestId('quick-edit').textContent).toBe('-'); + expect(screen.getByTestId('reference-pick-warning').textContent).toBe( + '快速编辑结果不支持改造,请继续使用快速编辑。', ); }); @@ -5150,7 +5190,10 @@ describe('useImageCanvasGenerationWorkflow', () => { fireEvent.click(screen.getByRole('button', { name: '打开图片改造' })); - expect(screen.getByTestId('dialog').textContent).not.toBe('-'); + expect(screen.getByTestId('dialog').textContent).toBe( + 'generate:idle:open:-:placeholder', + ); + expect(screen.getByTestId('quick-edit').textContent).toBe('-'); expect(screen.getByTestId('reference-pick-warning').textContent).toBe( '已按旧版数据恢复,部分参数可能使用当前默认值。', ); diff --git a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts index 98c6cbfaf..8ada24591 100644 --- a/src/components/image-editor/useImageCanvasGenerationWorkflow.ts +++ b/src/components/image-editor/useImageCanvasGenerationWorkflow.ts @@ -69,10 +69,10 @@ import { createEditDialogDraft, createGenerateDialogDraft, createIconGenerationDialogDraft, + createLegacyImageRedrawGenerationDialogDraft, createPublicationGenerationDialogDraft, createQuickEditGenerationDialogDraft, createQuickEditPanelDraft, - createRedrawPanelDraft, createSameSourceGenerationDialogDraft, createSceneGenerationDialogDraft, createSoundEffectGenerationDialogDraft, @@ -1084,14 +1084,15 @@ export function useImageCanvasGenerationWorkflow({ const refreshTaskList = useCallback(() => { setTaskListRefreshKey((key) => key + 1); }, []); - const backgroundMusicPromptAssist = - useImageCanvasBackgroundMusicPromptAssist({ + const backgroundMusicPromptAssist = useImageCanvasBackgroundMusicPromptAssist( + { canvasGenerationDialogs, getCanvasGenerationDialogById, updateCanvasGenerationDialogById, currentUserId, projectId, - }); + }, + ); const soundEffectPromptAssist = useImageCanvasSoundEffectPromptAssist({ canvasGenerationDialogs, getCanvasGenerationDialogById, @@ -1850,10 +1851,10 @@ export function useImageCanvasGenerationWorkflow({ ); const hasInvalidVersionedGenerationInputs = Boolean( sourceLayer.generationInputsHydrationState === 'invalid-versioned' || - sourceLayer.generationInputs && - (sourceLayer.generationInputs.version !== undefined || - sourceLayer.generationInputs.action !== undefined) && - !decodedGenerationInputs.ok, + (sourceLayer.generationInputs && + (sourceLayer.generationInputs.version !== undefined || + sourceLayer.generationInputs.action !== undefined) && + !decodedGenerationInputs.ok), ); if (hasInvalidVersionedGenerationInputs) { showGenerationWarning( @@ -1865,6 +1866,10 @@ export function useImageCanvasGenerationWorkflow({ ? decodedGenerationInputs.inputs : null; const isNormalizedGenerationInputs = normalizedGenerationInputs !== null; + if (normalizedGenerationInputs?.action === 'image.edit') { + showGenerationWarning('快速编辑结果不支持改造,请继续使用快速编辑。'); + return; + } const hasParameterFallbacks = decodedGenerationInputs.ok ? decodedGenerationInputs.warnings.length > 0 : false; @@ -1994,9 +1999,7 @@ export function useImageCanvasGenerationWorkflow({ selectSingleLayer(sourceLayer.id); openCanvasGenerationDialog(audioDraft); setActiveTool('music'); - showGenerationWarning( - '已按旧版数据恢复,部分参数可能使用当前默认值。', - ); + showGenerationWarning('已按旧版数据恢复,部分参数可能使用当前默认值。'); return; } if (sourceLayer.mediaType === 'video') { @@ -2008,9 +2011,7 @@ export function useImageCanvasGenerationWorkflow({ selectSingleLayer(sourceLayer.id); openCanvasGenerationDialog(videoDraft); setActiveTool('video'); - showGenerationWarning( - '已按旧版数据恢复,部分参数可能使用当前默认值。', - ); + showGenerationWarning('已按旧版数据恢复,部分参数可能使用当前默认值。'); return; } if ( @@ -2022,36 +2023,18 @@ export function useImageCanvasGenerationWorkflow({ showGenerationWarning('未找到角色动作关联的原角色图层,无法改造。'); return; } - const redrawPanel = createRedrawPanelDraft(sourceLayer, { - imageModel: sourceDialog?.imageModel, - aspectRatio: sourceDialog?.aspectRatio, - imageSize: sourceDialog?.imageSize, - }); - const redrawSize = resolveEditorImageGenerationPixelSize({ - model: redrawPanel.model, - aspectRatio: redrawPanel.aspectRatio, - imageSize: redrawPanel.imageSize, - }); - openPlacedCanvasGenerationDialog({ - ...createQuickEditGenerationDialogDraft({ + openPlacedCanvasGenerationDialog( + createLegacyImageRedrawGenerationDialogDraft({ sourceLayer, - prompt: redrawPanel.prompt, - model: redrawPanel.model, - aspectRatio: redrawPanel.aspectRatio, - imageSize: redrawPanel.imageSize, - frame: { - width: redrawSize.width, - height: redrawSize.height, - }, + canvasSize, + viewport, + sourceDialog, }), - composerOpen: true, - }); + ); setQuickEditPanel(null); selectSingleLayer(null); setActiveTool('generate'); - showGenerationWarning( - '已按旧版数据恢复,部分参数可能使用当前默认值。', - ); + showGenerationWarning('已按旧版数据恢复,部分参数可能使用当前默认值。'); return; } },