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(