From 3cbc33bd69c326e52f0669ba7219593398914f01 Mon Sep 17 00:00:00 2001 From: kdletters Date: Mon, 13 Jul 2026 18:48:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E7=94=BB=E5=B8=83=E5=BF=AB?= =?UTF-8?q?=E9=80=9F=E7=BC=96=E8=BE=91=E5=8F=82=E6=95=B0=E4=B8=8E=E6=9B=BF?= =?UTF-8?q?=E6=8D=A2=E8=A7=84=E5=88=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 快速编辑继承并支持模型、比例和尺寸参数,旧模型回落当前默认模型 仅禁止图标与图标图集快速编辑,图标规范继续按普通图片处理 按用户选定分辨率替换当前图层并保持图层中心位置 补齐后端真实图层校验、移动端避让、定向测试与项目文档 --- docs/project-memory/shared-memory/pitfalls.md | 6 +- ...】生成类面板Lovart统一改造方案-2026-06-17.md | 8 +- .../crates/api-server/src/editor_project.rs | 199 +++++++++++++++++- ...CanvasBasicGenerationComposerView.test.tsx | 20 +- ...ImageCanvasBasicGenerationComposerView.tsx | 8 +- .../ImageCanvasContextMenusView.test.tsx | 26 +++ .../ImageCanvasContextMenusView.tsx | 45 ++-- .../ImageCanvasEditorView.test.tsx | 17 ++ .../image-editor/ImageCanvasEditorView.tsx | 13 +- ...ImageCanvasGenerationComposerView.test.tsx | 8 +- .../ImageCanvasGenerationDialogModel.test.ts | 176 +++++++++------- .../ImageCanvasGenerationDialogModel.ts | 52 +++-- .../ImageCanvasGenerationLayerModel.test.ts | 16 +- .../ImageCanvasGenerationLayerModel.ts | 50 +++-- .../ImageCanvasGenerationModel.test.ts | 12 +- .../ImageCanvasGenerationModel.ts | 8 +- .../ImageCanvasQuickEditPanelView.test.tsx | 16 +- .../ImageCanvasQuickEditPanelView.tsx | 19 +- ...ageCanvasSelectedLayerToolbarView.test.tsx | 47 ++++- .../ImageCanvasSelectedLayerToolbarView.tsx | 31 +-- ...anvasGenerationSubmissionWorkflow.test.tsx | 29 ++- ...ImageCanvasGenerationSubmissionWorkflow.ts | 33 ++- .../useImageCanvasGenerationWorkflow.test.tsx | 8 +- .../useImageCanvasGenerationWorkflow.ts | 6 + src/index.css | 16 +- 25 files changed, 646 insertions(+), 223 deletions(-) diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index 43d9cf796..18ca6449a 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -368,12 +368,12 @@ - 验证:`npm run test -- src/services/image-editor/editorProjectClient.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx -- --runInBand`;后端验证至少覆盖 `editor_image_edit_request_omits_price_mud_points` 和 `editor_image_edit_can_complete_by_replacing_target_layer`。 - 关联:`src/services/image-editor/editorProjectClient.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`server-rs/crates/api-server/src/editor_project.rs`。 -## 图片画布快速编辑尺寸要区分业务目标和 provider 对齐尺寸 +## 图片画布快速编辑尺寸要区分用户目标和 provider 对齐尺寸 - 现象:原图经过快速编辑后 Resolution 变成近似比例的 1K / 2K 预设;原图或框选标记图宽高不是 16 的倍数时,VectorEngine edits 直接拒绝请求。 - 原因:前端已有源图精确 `originalWidth/originalHeight`,提交时却按最近常用比例和 K 档重新计算 `size`;后端又把非 16 倍数的目标尺寸和原始参考图字节直接放进 multipart,并以 provider 回图宽高落库和覆盖画布图层。 -- 处理:画布快速编辑提交源图精确尺寸作为业务目标;api-server 只在 provider 边界向右、向下复制边缘像素,把每张参考图和目标尺寸临时补齐到 16 的倍数,收到回图后裁回业务目标尺寸再持久化。若上游异常返回其他尺寸,先按目标比例裁切缩放;临时对齐尺寸不能进入 OSS 元数据、`editor_project_resource`、`editor_asset` 或画布 Resolution。前端 inline 回填也保留源图显示尺寸和 Resolution,避免旧回包再次放大图层。 -- 验证:`npm run test -- src/components/image-editor/ImageCanvasGenerationSubmissionModel.test.ts src/components/image-editor/ImageCanvasGenerationLayerModel.test.ts src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx` 覆盖 `1537x1025` 精确提交和源图回填;`cargo test -p api-server editor_image_edit_aligns_provider_images_and_restores_source_dimensions --manifest-path server-rs/Cargo.toml` 覆盖 provider `1552x1040`、额外参考图独立对齐和回图恢复。 +- 处理:画布快速编辑展示与常规图片生成一致的模型、比例和尺寸参数,默认继承来源生成器参数;缺少来源生成器时使用图层模型,并按真实分辨率推导比例和尺寸。用户当前选定的比例和尺寸共同决定业务目标分辨率,允许覆盖源图旧分辨率;api-server 只在 provider 边界向右、向下复制边缘像素,把每张参考图和目标尺寸临时补齐到 16 的倍数,收到回图后裁回业务目标尺寸再持久化。若上游异常返回其他尺寸,先按目标比例裁切缩放;临时对齐尺寸不能进入 OSS 元数据、`editor_project_resource`、`editor_asset` 或画布 Resolution。结果覆盖目标图层时更新原始分辨率,并保持图层中心位置不跳动。 +- 验证:`npm run test -- src/components/image-editor/ImageCanvasGenerationDialogModel.test.ts src/components/image-editor/ImageCanvasQuickEditPanelView.test.tsx src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.test.tsx` 覆盖来源参数继承、模型参数切换、目标尺寸提交和图层回填;`cargo test -p api-server editor_image_edit --manifest-path server-rs/Cargo.toml` 覆盖图标类拒绝、provider 尺寸对齐和回图恢复。 - 关联:`src/components/image-editor/ImageCanvasGenerationSubmissionModel.ts`、`src/components/image-editor/useImageCanvasGenerationSubmissionWorkflow.ts`、`src/components/image-editor/ImageCanvasGenerationLayerModel.ts`、`server-rs/crates/api-server/src/editor_project.rs`。 ## 图片画布快速编辑元数据必须记录原图引用 diff --git a/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md b/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md index 3c59dfd14..afd2182c6 100644 --- a/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md +++ b/docs/【编辑器】生成类面板Lovart统一改造方案-2026-06-17.md @@ -18,7 +18,7 @@ - `快速编辑` `角色动画生成面板` 同步纳入本次生成类面板交互统一:点击角色图只聚焦图层,不自动弹出底部重绘或角色动画面板;点击 `生成动画` 后像新建图片一样创建 `角色动作` 画布占位,面板跟随占位底部,参考图首行、单文本无边界、参数按钮向上弹出、生成按钮明确展示泥点。 -`快速编辑` 由选中图片后的浮动工具栏显式打开,图片类素材统一进入框选区域 + 单提示词 + 模型选择的修改面板,不再恢复原来源生成器,也不展示参考图或尺寸控件。 +`快速编辑` 由选中图片后的浮动工具栏显式打开,图片类素材统一进入框选区域 + 单提示词 + 比例 / 尺寸 + 模型选择的修改面板,不再恢复原来源生成器,也不展示额外参考图。`icon` 与 `icon-spritesheet` 图标类素材不支持快速编辑,`icon-spec` 图标规范仍按普通图片支持快速编辑。 ## 统一布局 @@ -68,7 +68,7 @@ - 生成图片和生成视频文本输入框紧贴参考图下方,取消旧网格预留导致的空白高度。 - 生成规范类图片固定使用 `16:9·2K · gpt-image-2`。这三个参数在面板底部沿用可编辑参数按钮的胶囊样式展示,但控件保持禁用不可点击,不提供比例、尺寸或模型修改入口。 - 宣发素材的 `游戏首图`、`详情五图`、`运营海报` 固定使用 `gpt-image-2`。面板底部只显示禁用态 `gpt-image-2` 模型胶囊和生成按钮,不出现 `nanobanana2` 选项;后端收到 `publication-material` 旧请求时也必须强制归一为 `gpt-image-2`。 -- 图片快速编辑只保留一个提示词输入框和模型选择;提示词 placeholder 为 `写下每个编号要怎么改`,提交按钮显示 `修改`,不展示比例 / 尺寸或参考图控件。 +- 图片快速编辑保留一个提示词输入框,并展示与常规图片生成一致的比例 / 尺寸和模型选择;提示词 placeholder 为 `你希望素材如何修改?`,提交按钮显示 `修改`,不展示额外参考图控件。打开面板时优先继承原图关联生成器记录的模型、比例和尺寸;没有关联生成器时使用图层模型,并按原图真实分辨率推导比例和尺寸;模型缺失或已不受支持时回落到当前默认图片模型。切换模型后只展示该模型支持的参数,不兼容的当前值回落到该模型默认值,按钮泥点按选定模型和尺寸同步刷新。 - 不再在底部常驻展开全部可选项。 ## 泥点显示 @@ -99,7 +99,7 @@ - 图片类待生成占位尺寸必须与面板当前比例和尺寸同步:普通图片、角色形象、图标素材、UI 设计图按当前 `aspectRatio + imageSize` 计算像素尺寸;生成规范固定为 `16:9·2K`,占位为 `2048 x 1152`;宣发素材按 workflow 输出尺寸创建占位。 - 视频待生成占位必须与面板当前比例和清晰度同步:默认 `16:9 · 480p` 为 `854 x 480`,切换比例、`720p` 或 `1080p` 后按比例和清晰度重算偶数宽度;调整参数时保持占位中心点不变。 - 面板中用户修改比例、尺寸或清晰度后,已有空白待生成占位立即同步更新 `width / height / originalWidth / originalHeight`,且保持中心点不跳动。 -- 快速编辑点击修改后不创建独立 `Quick Edit Generator` 画布生成占位;当前快速编辑面板显示修改中,生成成功后结果直接覆盖源图,失败时保留当前面板并显示错误。需要新建占位的是生成图片、生成视频、重绘、去背景和角色动作等会产出新图层的入口。 +- 快速编辑点击修改后不创建独立 `Quick Edit Generator` 画布生成占位;当前快速编辑面板显示修改中,生成成功后结果直接覆盖源图,失败时保留当前面板并显示错误。用户选定的比例和尺寸是新的业务目标分辨率,覆盖旧的“始终保持源图精确分辨率”约束;替换时更新图层原始分辨率,并保持图层中心位置不跳动。需要新建占位的是生成图片、生成视频、重绘、去背景和角色动作等会产出新图层的入口。 - 任何会打开画布内 composer / 面板的入口,必须在面板渲染后通过统一 overlay 可见性校正检查真实 DOM 矩形;如果面板超出画布视口,或底部工具栏 / 左下 dock 会遮住面板,就只平移当前 viewport 让面板完整进入安全区域。新增生成类入口不要在按钮 handler 里手写单独的避让偏移。 - 画布Agent对话入口例外:画布Agent对话(右侧对话面板)触发的生成不创建"即将生成"画布占位,生成中状态由对话消息流内的条目承载(阶段提示、模型标注、进行中动画);生成完成后结果图才按统一 placement 避让模型落画板为新图层,并在对话消息内显示缩略图。工具失败时也必须在对话消息内保留失败 generation record,而不是只弹一次性错误气泡。该例外仅限画布Agent对话入口,其余生成入口仍必须先落占位。详见 docs/【编辑器】画布Agent对话面板-2026-07-03.md。 @@ -178,7 +178,7 @@ - `生成音乐` 选项面板出现在音乐按钮上方,不再固定在底栏中间。 - 规范面板比图片生成面板更紧凑,字段间距和输入高度更小,但外层 shell、首行参考图和底部按钮区必须继续对齐生成图片 / 生成角色 / 生成视频。 - 生成规范类图片底部展示禁用态参数按钮 `16:9·2K` 和 `gpt-image-2`,视觉对齐可编辑面板的比例 / 尺寸 / 模型按钮;提交参数也固定为这三项,不出现可展开选项。 -- 图片快速编辑底部只展示模型选择和 `修改` 按钮;原图或红框序号标注图作为 `sourceImageSrc` 直接编辑,不展示参考图条或比例 / 尺寸控件。 +- 图片快速编辑底部左侧展示比例 / 尺寸组合选择,右侧展示模型选择和 `修改` 按钮;原图或红框序号标注图作为 `sourceImageSrc` 直接编辑,不展示额外参考图条。图标与图集素材不展示快速编辑入口,图标规范仍可快速编辑。 - 快速编辑打开后画布自动缩放平移到原图完整展示,并让面板位于原图下方且不遮挡原图;原图右侧出现竖向矩形 / 椭圆 / 画笔自由框选按钮。进入快速编辑不默认启用框选,点击工具启用并保持高亮,再点同一工具取消;完成框选后画布红色细框显示连续序号,输入框同步追加 `对N号红色圈选框里的内容做以下修改:`。 - 快速编辑提交前保留提示词里对原图的 `原图`、`当前图片`、`当前图` 或 `图1` 引用,不再改写成 `图N`。 - 快速编辑提交给后端时只把原图或已绘制红框和序号的标注图作为 `sourceImageSrc`,不提交隐藏的 `referenceImageSrcs`。 diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 1f484b7d3..87b6de8d1 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -1967,6 +1967,93 @@ fn align_editor_image_edit_dimension(value: u32) -> u32 { value.saturating_add(15) / 16 * 16 } +fn ensure_editor_image_edit_asset_kind_allowed(asset_kind: Option<&str>) -> Result<(), AppError> { + let Some(asset_kind) = asset_kind.map(str::trim).filter(|value| !value.is_empty()) else { + return Ok(()); + }; + if !matches!(asset_kind, "icon" | "icon-spritesheet") { + return Ok(()); + } + + Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "editor-image-edit", + "message": "图标与图标图集不支持快速编辑", + "assetKind": asset_kind, + })), + ) +} + +fn resolve_editor_image_edit_target_layer_asset_kind<'a>( + layers: &'a Value, + resources: &'a [EditorProjectResourcePayload], + target_layer_id: &str, +) -> Result, AppError> { + let target_layer = layers + .as_array() + .and_then(|items| { + items + .iter() + .find(|item| item.get("layerId").and_then(Value::as_str) == Some(target_layer_id)) + }) + .ok_or_else(|| { + AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({ + "provider": "editor-image-edit", + "message": "快速编辑目标图层不存在", + "targetLayerId": target_layer_id, + })) + })?; + if let Some(asset_kind) = target_layer.get("assetKind").and_then(Value::as_str) { + return Ok(Some(asset_kind)); + } + let resource_id = target_layer.get("resourceId").and_then(Value::as_str); + Ok(resource_id.and_then(|resource_id| { + resources + .iter() + .find(|resource| resource.resource_id == resource_id) + .and_then(|resource| resource.asset_kind.as_deref()) + })) +} + +async fn ensure_editor_image_edit_source_allowed( + state: &AppState, + owner_user_id: &str, + payload: &EditorImageEditRequest, +) -> Result<(), AppError> { + let Some(project_id) = payload + .project_id + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + else { + return ensure_editor_image_edit_asset_kind_allowed(payload.asset_kind.as_deref()); + }; + let Some(target_layer_id) = payload + .target_layer_id + .as_deref() + .map(str::trim) + .filter(|value| !value.is_empty()) + else { + return ensure_editor_image_edit_asset_kind_allowed(payload.asset_kind.as_deref()); + }; + let project = state + .spacetime_client() + .get_editor_project(EditorProjectGetRecordInput { + project_id: project_id.to_string(), + owner_user_id: owner_user_id.to_string(), + }) + .await + .map_err(map_editor_project_error)?; + let project = editor_project_payload_from_record(project); + let asset_kind = resolve_editor_image_edit_target_layer_asset_kind( + &project.layers, + &project.resources, + target_layer_id, + )? + .or(payload.asset_kind.as_deref()); + ensure_editor_image_edit_asset_kind_allowed(asset_kind) +} + fn encode_editor_image_edit_png( image: image::DynamicImage, failure_status: StatusCode, @@ -2105,6 +2192,8 @@ pub async fn edit_editor_image( ) -> Result, AppError> { let caller = EditorGenerationCaller::from_authenticated(&authenticated); if !state.config.external_generation_mode.is_inline() { + ensure_editor_image_edit_source_allowed(&state, caller.owner_user_id.as_str(), &payload) + .await?; let generation_options = normalize_editor_generation_options(payload.model.as_deref(), None, None); let image_size = normalize_editor_image_generation_size(payload.size.as_deref()); @@ -2141,6 +2230,7 @@ pub(crate) async fn edit_editor_image_for_owner( caller: EditorGenerationCaller, payload: EditorImageEditRequest, ) -> Result, AppError> { + ensure_editor_image_edit_source_allowed(state, caller.owner_user_id.as_str(), &payload).await?; let prompt = payload.prompt.trim().to_string(); if prompt.is_empty() { return Err( @@ -2275,6 +2365,7 @@ pub(crate) async fn edit_editor_image_for_owner( payload.project_id.as_deref(), payload.target_layer_id.as_deref(), generated_asset.resource.as_ref(), + true, ) .await? }; @@ -2403,6 +2494,7 @@ pub(crate) async fn remove_editor_image_background_for_owner( payload.project_id.as_deref(), payload.target_layer_id.as_deref(), generated_asset.resource.as_ref(), + false, ) .await? }; @@ -4603,6 +4695,7 @@ pub(crate) async fn complete_editor_canvas_background_removal( project_id: Option<&str>, target_layer_id: Option<&str>, resource: Option<&EditorProjectResourcePayload>, + recenter_replacement: bool, ) -> Result, AppError> { let (Some(project_id), Some(target_layer_id), Some(resource)) = (project_id, target_layer_id, resource) @@ -4630,6 +4723,7 @@ pub(crate) async fn complete_editor_canvas_background_removal( project_payload.layers, target_layer_id, resource, + recenter_replacement, ) else { return Ok(None); }; @@ -4653,6 +4747,7 @@ fn replace_editor_canvas_layer_with_resource( layers: Value, target_layer_id: &str, resource: &EditorProjectResourcePayload, + recenter_replacement: bool, ) -> Option { let mut items = match layers { Value::Array(items) => items, @@ -4671,11 +4766,34 @@ fn replace_editor_canvas_layer_with_resource( continue; } + let replacement_width = f64::from(resource.width.max(1)); + let replacement_height = f64::from(resource.height.max(1)); + let centered_position = recenter_replacement + .then(|| { + object + .get("x") + .and_then(Value::as_f64) + .zip(object.get("y").and_then(Value::as_f64)) + .zip(object.get("width").and_then(Value::as_f64)) + .zip(object.get("height").and_then(Value::as_f64)) + .map(|(((x, y), width), height)| { + ( + x + width / 2.0 - replacement_width / 2.0, + y + height / 2.0 - replacement_height / 2.0, + ) + }) + }) + .flatten(); + object.insert( "resourceId".to_string(), Value::String(resource.resource_id.clone()), ); object.insert("src".to_string(), Value::String(resource.image_src.clone())); + if let Some((x, y)) = centered_position { + object.insert("x".to_string(), json!(x)); + object.insert("y".to_string(), json!(y)); + } object.insert("width".to_string(), json!(resource.width.max(1))); object.insert("height".to_string(), json!(resource.height.max(1))); object.insert("originalWidth".to_string(), json!(resource.width.max(1))); @@ -7193,8 +7311,13 @@ mod tests { updated_at: "2026-06-29T00:00:00.000Z".to_string(), }; - let replaced = replace_editor_canvas_layer_with_resource(layers, "layer-source", &resource) - .expect("target layer should be replaced"); + let replaced = replace_editor_canvas_layer_with_resource( + layers.clone(), + "layer-source", + &resource, + false, + ) + .expect("target layer should be replaced"); let items = replaced.as_array().expect("layers should stay array"); let layer = items .iter() @@ -7218,6 +7341,18 @@ mod tests { assert_eq!(layer["assetKind"], json!("character")); assert!(layer.get("sourceAssetId").is_none()); assert!(layer.get("thumbnailSrc").is_none()); + + let recentered = + replace_editor_canvas_layer_with_resource(layers, "layer-source", &resource, true) + .expect("quick edit replacement should recenter the layer"); + let recentered_layer = recentered + .as_array() + .and_then(|items| items.iter().find(|item| item["layerId"] == "layer-source")) + .expect("quick edit source layer should remain"); + assert_eq!(recentered_layer["x"], json!(24.0)); + assert_eq!(recentered_layer["y"], json!(-124.0)); + assert_eq!(recentered_layer["x"].as_f64().unwrap() + 512.0 / 2.0, 280.0); + assert_eq!(recentered_layer["y"].as_f64().unwrap() + 768.0 / 2.0, 260.0); } #[test] @@ -8069,6 +8204,66 @@ mod tests { assert_eq!(request.target_layer_id.as_deref(), Some("layer-source")); } + #[test] + fn editor_image_edit_rejects_icons_but_allows_icon_specs() { + for asset_kind in ["icon", "icon-spritesheet"] { + let error = ensure_editor_image_edit_asset_kind_allowed(Some(asset_kind)) + .expect_err("icon assets should not support quick edit"); + assert_eq!(error.status_code(), StatusCode::BAD_REQUEST); + assert_eq!( + error.details().and_then(|details| details.get("message")), + Some(&json!("图标与图标图集不支持快速编辑")), + ); + assert_eq!( + error.details().and_then(|details| details.get("assetKind")), + Some(&json!(asset_kind)), + ); + } + + assert!(ensure_editor_image_edit_asset_kind_allowed(Some("icon-spec")).is_ok()); + assert!(ensure_editor_image_edit_asset_kind_allowed(Some("image")).is_ok()); + assert!(ensure_editor_image_edit_asset_kind_allowed(None).is_ok()); + + let layers = json!([ + { "layerId": "layer-icon", "assetKind": "icon" }, + { "layerId": "layer-icon-spec", "assetKind": "icon-spec" } + ]); + let icon_kind = + resolve_editor_image_edit_target_layer_asset_kind(&layers, &[], "layer-icon") + .expect("icon layer should exist"); + assert!(ensure_editor_image_edit_asset_kind_allowed(icon_kind).is_err()); + let icon_spec_kind = + resolve_editor_image_edit_target_layer_asset_kind(&layers, &[], "layer-icon-spec") + .expect("icon spec layer should exist"); + assert!(ensure_editor_image_edit_asset_kind_allowed(icon_spec_kind).is_ok()); + assert!( + resolve_editor_image_edit_target_layer_asset_kind(&layers, &[], "missing").is_err() + ); + } + + #[test] + fn editor_image_edit_guards_before_queue_and_owner_execution() { + let source = include_str!("editor_project.rs"); + assert_function_contains_in_order( + source, + "pub async fn edit_editor_image(", + "pub(crate) async fn edit_editor_image_for_owner", + &[ + "ensure_editor_image_edit_source_allowed(", + "enqueue_editor_generation_job", + ], + ); + assert_function_contains_in_order( + source, + "pub(crate) async fn edit_editor_image_for_owner", + "pub async fn remove_editor_image_background", + &[ + "ensure_editor_image_edit_source_allowed(", + "parse_editor_reference_image", + ], + ); + } + #[test] fn editor_image_edit_can_complete_by_replacing_target_layer() { let source = include_str!("editor_project.rs"); diff --git a/src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx b/src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx index 90335a5c6..a5bf9c096 100644 --- a/src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx +++ b/src/components/image-editor/ImageCanvasBasicGenerationComposerView.test.tsx @@ -183,7 +183,7 @@ describe('ImageCanvasBasicGenerationComposerView', () => { expect(submitButton.textContent).toBe('生成5泥点'); }); - it('keeps quick edit to one prompt box and model selection', () => { + it('keeps quick edit to one prompt box with image parameters', () => { render( { ).toBeNull(); expect(within(panel).queryByText('旧参考图')).toBeNull(); expect( - within(panel).queryByRole('button', { - name: /快速编辑图片尺寸/u, + within(panel).getByRole('button', { + name: '快速编辑图片尺寸 1:1·1K', }), - ).toBeNull(); + ).toBeTruthy(); expect( within(panel).getByRole('button', { name: '快速编辑图片模型 gpt-image-2', @@ -234,22 +234,16 @@ describe('ImageCanvasBasicGenerationComposerView', () => { name: '生成图片尺寸 16:9·1K', }), ); - expect( - screen.getByRole('menu', { name: '生成图片尺寸选项' }), - ).toBeTruthy(); + expect(screen.getByRole('menu', { name: '生成图片尺寸选项' })).toBeTruthy(); fireEvent.click(screen.getByRole('textbox', { name: '生成提示词' })); - expect( - screen.queryByRole('menu', { name: '生成图片尺寸选项' }), - ).toBeNull(); + expect(screen.queryByRole('menu', { name: '生成图片尺寸选项' })).toBeNull(); }); it('does not render a standalone close button', () => { render(); - expect( - screen.queryByRole('button', { name: '关闭生成图片' }), - ).toBeNull(); + expect(screen.queryByRole('button', { name: '关闭生成图片' })).toBeNull(); }); }); diff --git a/src/components/image-editor/ImageCanvasBasicGenerationComposerView.tsx b/src/components/image-editor/ImageCanvasBasicGenerationComposerView.tsx index 4a6a11b6f..ce63f14c6 100644 --- a/src/components/image-editor/ImageCanvasBasicGenerationComposerView.tsx +++ b/src/components/image-editor/ImageCanvasBasicGenerationComposerView.tsx @@ -140,19 +140,21 @@ export function ImageCanvasBasicGenerationComposerView({ const references = dialog.generationReferences ?? []; const isQuickEdit = dialog.mode === 'quick-edit'; const resolvedDialogLabel = - dialogLabel ?? optionLabelPrefix ?? (isQuickEdit ? '快速编辑图片' : '生成图片'); + dialogLabel ?? + optionLabelPrefix ?? + (isQuickEdit ? '快速编辑图片' : '生成图片'); const resolvedPromptLabel = promptLabel ?? (isQuickEdit ? '快速编辑提示词' : '生成提示词'); const resolvedPromptPlaceholder = promptPlaceholder ?? - (isQuickEdit ? '写下每个编号要怎么改' : '今天想生成什么画面?'); + (isQuickEdit ? '你希望素材如何修改?' : '今天想生成什么画面?'); const resolvedOptionLabelPrefix = optionLabelPrefix ?? (isQuickEdit ? '快速编辑图片' : '生成图片'); const resolvedReferenceButtonLabel = referenceButtonLabel; const resolvedReferenceButtonAriaLabel = referenceButtonAriaLabel ?? `添加${resolvedReferenceButtonLabel}`; const shouldIncludeReferences = isQuickEdit ? false : includeReferences; - const shouldIncludeDimensions = isQuickEdit ? false : includeDimensions; + const shouldIncludeDimensions = includeDimensions; const resolvedSubmitLabel = isQuickEdit && submitLabel === '生成' ? '修改' : submitLabel; const resolvedSubmitAriaLabel = diff --git a/src/components/image-editor/ImageCanvasContextMenusView.test.tsx b/src/components/image-editor/ImageCanvasContextMenusView.test.tsx index d56551386..824bb0301 100644 --- a/src/components/image-editor/ImageCanvasContextMenusView.test.tsx +++ b/src/components/image-editor/ImageCanvasContextMenusView.test.tsx @@ -265,4 +265,30 @@ describe('ImageCanvasContextMenusView', () => { expect(props.onCloseImageContextMenu).toHaveBeenCalledTimes(2); expect(props.onDeleteLayerById).toHaveBeenCalledWith(layer.id); }); + + it.each(['icon', 'icon-spritesheet'] as const)( + 'hides quick edit from the standalone menu for %s assets', + (assetKind) => { + const layer = createLayer({ assetKind }); + renderContextMenus({ + imageContextMenu: { layerId: layer.id, x: 20, y: 22 }, + imageContextMenuLayer: layer, + }); + + expect(screen.queryByRole('menuitem', { name: '快速编辑' })).toBeNull(); + expect( + screen.getByRole('menuitem', { name: '查看图片信息' }), + ).toBeTruthy(); + }, + ); + + it('keeps quick edit in the standalone menu for icon specs', () => { + const layer = createLayer({ assetKind: 'icon-spec' }); + renderContextMenus({ + imageContextMenu: { layerId: layer.id, x: 20, y: 22 }, + imageContextMenuLayer: layer, + }); + + expect(screen.getByRole('menuitem', { name: '快速编辑' })).toBeTruthy(); + }); }); diff --git a/src/components/image-editor/ImageCanvasContextMenusView.tsx b/src/components/image-editor/ImageCanvasContextMenusView.tsx index 98d170d32..91c143bf6 100644 --- a/src/components/image-editor/ImageCanvasContextMenusView.tsx +++ b/src/components/image-editor/ImageCanvasContextMenusView.tsx @@ -18,6 +18,7 @@ import type { CanvasViewport, ImageContextMenuState, } from './ImageCanvasEditorTypes'; +import { isQuickEditUnsupportedAssetKind } from './ImageCanvasGenerationModel'; type ImageCanvasContextMenusViewProps = { viewport: CanvasViewport; @@ -477,17 +478,19 @@ export function ImageCanvasContextMenusView({
{imageContextMenuLayer ? ( <> - + {!isQuickEditUnsupportedAssetKind(imageContextMenuLayer) ? ( + + ) : null}