diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/marker.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/marker.rs index 292725416..9c8cfd120 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/marker.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/marker.rs @@ -3,6 +3,8 @@ use base64::Engine as _; use std::path::Path; use std::path::PathBuf; +const MARKER_LINE_WIDTH: u32 = 2; + pub async fn build_marked_image( source_url: String, nodes: Vec, @@ -120,12 +122,144 @@ fn draw_frame( return; }; let green = image::Rgba([0, 255, 0, 255]); - for xx in x0..=x1 { - image.put_pixel(xx, y0, green); - image.put_pixel(xx, y1, green); - } - for yy in y0..=y1 { - image.put_pixel(x0, yy, green); - image.put_pixel(x1, yy, green); + draw_line( + image, + (x0, y0), + (x1, y0), + green, + MARKER_LINE_WIDTH, + (x0, y0, x1, y1), + ); + draw_line( + image, + (x0, y1), + (x1, y1), + green, + MARKER_LINE_WIDTH, + (x0, y0, x1, y1), + ); + draw_line( + image, + (x0, y0), + (x0, y1), + green, + MARKER_LINE_WIDTH, + (x0, y0, x1, y1), + ); + draw_line( + image, + (x1, y0), + (x1, y1), + green, + MARKER_LINE_WIDTH, + (x0, y0, x1, y1), + ); + draw_line( + image, + (x0, y0), + (x1, y1), + green, + MARKER_LINE_WIDTH, + (x0, y0, x1, y1), + ); + draw_line( + image, + (x1, y0), + (x0, y1), + green, + MARKER_LINE_WIDTH, + (x0, y0, x1, y1), + ); +} + +fn draw_line( + image: &mut image::RgbaImage, + start: (u32, u32), + end: (u32, u32), + color: image::Rgba, + line_width: u32, + bounds: (u32, u32, u32, u32), +) { + let mut x = start.0 as i64; + let mut y = start.1 as i64; + let target_x = end.0 as i64; + let target_y = end.1 as i64; + let dx = (target_x - x).abs(); + let sx = if x < target_x { 1 } else { -1 }; + let dy = -(target_y - y).abs(); + let sy = if y < target_y { 1 } else { -1 }; + let mut error = dx + dy; + + loop { + draw_brush(image, x, y, color, line_width, bounds); + if x == target_x && y == target_y { + break; + } + let twice_error = error * 2; + if twice_error >= dy { + error += dy; + x += sx; + } + if twice_error <= dx { + error += dx; + y += sy; + } + } +} + +fn draw_brush( + image: &mut image::RgbaImage, + x: i64, + y: i64, + color: image::Rgba, + line_width: u32, + bounds: (u32, u32, u32, u32), +) { + let (x0, y0, x1, y1) = bounds; + let line_width = line_width.max(1) as i64; + let before = (line_width - 1) / 2; + let after = line_width / 2; + let min_x = (x - before).max(x0 as i64); + let max_x = (x + after).min(x1 as i64); + let min_y = (y - before).max(y0 as i64); + let max_y = (y + after).min(y1 as i64); + for yy in min_y..=max_y { + for xx in min_x..=max_x { + image.put_pixel(xx as u32, yy as u32, color); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn draw_frame_adds_green_cross_corner_lines() { + let mut image = image::RgbaImage::from_pixel(8, 6, image::Rgba([1, 2, 3, 255])); + draw_frame(&mut image, 1, 1, 5, 3, 8, 6); + let green = image::Rgba([0, 255, 0, 255]); + + for &(x, y) in &[(1, 1), (5, 1), (1, 3), (5, 3), (3, 2)] { + assert_eq!(*image.get_pixel(x, y), green, "pixel ({x}, {y})"); + } + assert_eq!(*image.get_pixel(3, 1), green); + assert_eq!(*image.get_pixel(3, 3), green); + assert_eq!(*image.get_pixel(2, 2), green); + assert_eq!(*image.get_pixel(4, 2), green); + assert_eq!(*image.get_pixel(0, 0), image::Rgba([1, 2, 3, 255])); + } + + #[test] + fn draw_frame_keeps_cross_inside_clipped_rect() { + let mut image = image::RgbaImage::from_pixel(4, 4, image::Rgba([1, 2, 3, 255])); + draw_frame(&mut image, 2, 2, 4, 4, 4, 4); + let green = image::Rgba([0, 255, 0, 255]); + for y in 2..4 { + for x in 2..4 { + assert_eq!(*image.get_pixel(x, y), green); + } + } + assert_eq!(*image.get_pixel(1, 1), image::Rgba([1, 2, 3, 255])); } } diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/prompt.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/prompt.rs index 7781b6bf0..d6df93e9f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/prompt.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/prompt.rs @@ -5,8 +5,10 @@ const SHARED_SEPARATION_REQ: &str = r#" MUST hard edges; preserve no glow/blur beyond the exact visible shape. NEVER keep its parent's background with it. - UI elements that needs to extract has been marked with GREEN line frames (only for mark purpose, NEVER wrap a frame in your extraction). + UI elements that needs to extract has been marked with GREEN line frames box with crossline inside. (only for mark purpose, NEVER wrap a frame in your extraction). On some UI elements, there is some PURPLE filled area, they were removed UI elements, reconstruct the background under where they were. + + MUST extract exactly these marked UI elements area. "#; pub(super) fn gen_extract_prompt(separation_notes: Vec) -> String { let extract_system_prompt = format!( diff --git a/docs/technical/【技术方案】UI编辑器自动分离工作流-2026-09-08.md b/docs/technical/【技术方案】UI编辑器自动分离工作流-2026-09-08.md index 67b8a66a6..0c87b1cdc 100644 --- a/docs/technical/【技术方案】UI编辑器自动分离工作流-2026-09-08.md +++ b/docs/technical/【技术方案】UI编辑器自动分离工作流-2026-09-08.md @@ -29,7 +29,7 @@ ## 图片编辑与视觉绑定 -- image-edit 使用源 UI design 图片及由 Rust 生成的绿色标记/紫色重建输入。处理父节点时,紫色填充其 children 的矩形区域(包括已 problematic 的 children),再在父节点自身外围绘制绿色框;绿色框覆盖在紫色之上。叶节点只绘制绿色框,不填充自身。 +- image-edit 使用源 UI design 图片及由 Rust 生成的绿色标记/紫色重建输入。处理父节点时,紫色填充其 children 的矩形区域(包括已 problematic 的 children),再在父节点自身外围绘制绿色框和角到角的绿色交叉线;绿色标记覆盖在紫色之上。叶节点只绘制绿色框和角到角的绿色交叉线,不填充自身。 - 请求尺寸始终使用源 UI design 尺寸;Raw GPT Image 2 API 保证返回相同尺寸,客户端不额外做尺寸拒绝检查。 - 标记图构建、处理图解码/写入和 cut 裁切属于本地 CPU/文件操作,放入独立的 `spawn_blocking` 任务;image-edit 与 visual binding 网络请求仍运行在 async future 中。