From 56fff8325b3c6e1a154d9302f4fdf890cb04a05a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Wed, 9 Sep 2026 10:45:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B0=86=E8=87=AA=E5=8A=A8=E5=88=86=E7=A6=BB?= =?UTF-8?q?=E5=9B=BE=E5=83=8F=E5=A4=84=E7=90=86=E7=A7=BB=E5=87=BA=E5=BC=82?= =?UTF-8?q?=E6=AD=A5=E6=89=A7=E8=A1=8C=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 spawn_blocking 中构建标记图并编码写入处理图 在 spawn_blocking 中完成处理图裁切与文件输出 保留 image-edit 和视觉绑定网络请求的 async 调度边界 --- .../ui_editor/commands/separation/workflow.rs | 104 ++++++++++++++---- 1 file changed, 80 insertions(+), 24 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow.rs index a17c07926..19e1ee531 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/workflow.rs @@ -16,7 +16,7 @@ use platform_llm::{ }; use serde::Deserialize; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; pub fn apply_batch_patch( state: &mut SeparationState, tree_index: usize, @@ -167,9 +167,32 @@ async fn raw_image_edit( .ok_or_else(|| "图片分离响应没有图像".to_string()) } -fn build_marked_image( +async fn build_marked_image( + source_url: String, + nodes: Vec, + target: PathBuf, +) -> Result { + tokio::task::spawn_blocking(move || { + let areas = nodes + .iter() + .map(|node| { + ( + node.global_pos_x_px, + node.global_pos_y_px, + node.width_px, + node.height_px, + ) + }) + .collect::>(); + build_marked_image_blocking(&source_url, &areas, &target) + }) + .await + .map_err(|error| format!("构建标记图任务失败:{error}"))? +} + +fn build_marked_image_blocking( source_url: &str, - nodes: &[&SeparationNode], + nodes: &[(u32, u32, u32, u32)], target: &Path, ) -> Result { let encoded = source_url @@ -184,17 +207,15 @@ fn build_marked_image( .to_rgba8(); let width = image.width(); let height = image.height(); - for node in nodes { - let x0 = node.global_pos_x_px.min(width.saturating_sub(1)); - let y0 = node.global_pos_y_px.min(height.saturating_sub(1)); - let x1 = node - .global_pos_x_px - .saturating_add(node.width_px) + for &(global_pos_x_px, global_pos_y_px, node_width_px, node_height_px) in nodes { + let x0 = global_pos_x_px.min(width.saturating_sub(1)); + let y0 = global_pos_y_px.min(height.saturating_sub(1)); + let x1 = global_pos_x_px + .saturating_add(node_width_px) .min(width) .saturating_sub(1); - let y1 = node - .global_pos_y_px - .saturating_add(node.height_px) + let y1 = global_pos_y_px + .saturating_add(node_height_px) .min(height) .saturating_sub(1); if x0 >= x1 || y0 >= y1 { @@ -229,6 +250,23 @@ fn build_marked_image( )) } +async fn write_processed_image(processed_url: String, target: PathBuf) -> Result<(), String> { + tokio::task::spawn_blocking(move || { + let processed_bytes = base64::engine::general_purpose::STANDARD + .decode( + processed_url + .split_once(',') + .map(|(_, data)| data) + .unwrap_or_default(), + ) + .map_err(|error| format!("解析处理图失败:{error}"))?; + fs::write(&target, processed_bytes) + .map_err(|error| format!("写入处理图失败:{}: {error}", target.display())) + }) + .await + .map_err(|error| format!("写入处理图任务失败:{error}"))? +} + async fn visual_binding( source_url: String, processed_url: String, @@ -337,7 +375,13 @@ pub(crate) async fn separate_ui_impl( let prompt = gen_extract_prompt(batch.iter().map(|n| n.note.clone()).collect::>()); let marker_path = sidecar.join(format!("marked-{}.png", separation.bound.len())); - let marked_url = match build_marked_image(&source_url, &batch, &marker_path) { + let marked_url = match build_marked_image( + source_url.clone(), + batch_nodes.clone(), + marker_path, + ) + .await + { Ok(value) => value, Err(error) => { mark_batch_problematic(&mut separation, tree_index, &batch, error); @@ -362,15 +406,7 @@ pub(crate) async fn separate_ui_impl( } }; let processed_path = sidecar.join(format!("processed-{}.png", separation.bound.len())); - let processed_bytes = base64::engine::general_purpose::STANDARD - .decode( - processed_url - .split_once(',') - .map(|(_, d)| d) - .unwrap_or_default(), - ) - .map_err(|e| e.to_string())?; - fs::write(&processed_path, processed_bytes).map_err(|e| e.to_string())?; + write_processed_image(processed_url.clone(), processed_path.clone()).await?; let binding = match visual_binding(source_url.clone(), processed_url, &batch).await { Ok(value) => value, Err(error) => { @@ -388,7 +424,13 @@ pub(crate) async fn separate_ui_impl( } = decision { let cut_path = sidecar.join(format!("cut-{}.png", to_node.as_str())); - match cut_processed_image(&processed_path, separated_image_area, &cut_path) { + match cut_processed_image( + processed_path.clone(), + *separated_image_area, + cut_path.clone(), + ) + .await + { Ok(()) => { cut_paths .insert(to_node.clone(), cut_path.to_string_lossy().to_string()); @@ -414,7 +456,21 @@ pub(crate) async fn separate_ui_impl( Ok(separation_dto(&separation)) } -fn cut_processed_image(source: &Path, area: &BindingArea, target: &Path) -> Result<(), String> { +async fn cut_processed_image( + source: PathBuf, + area: BindingArea, + target: PathBuf, +) -> Result<(), String> { + tokio::task::spawn_blocking(move || cut_processed_image_blocking(&source, &area, &target)) + .await + .map_err(|error| format!("裁切处理图任务失败:{error}"))? +} + +fn cut_processed_image_blocking( + source: &Path, + area: &BindingArea, + target: &Path, +) -> Result<(), String> { let image = image::open(source).map_err(|e| format!("读取处理图失败:{e}"))?; area.validate_in(image.width(), image.height())?; let cropped = image.crop_imm(