From 56f8c9a6a17fd3d116d5e1cad0d9dd100b334292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 10 Sep 2026 10:49:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9E=84=E9=80=A0=E8=87=AA=E5=8A=A8=E5=88=86?= =?UTF-8?q?=E7=A6=BB=E6=96=87=E5=AD=97=E9=81=AE=E7=BD=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按最近未绑定图片祖先收集 Text 布局像素矩形 补充嵌套图片与根图片遮罩构造测试 --- .../src/ui_editor/commands/separation/mod.rs | 64 +++++++++++++++++++ .../src/ui_editor/commands/separation/tree.rs | 52 ++++++++++++++- 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/mod.rs index 91dc38009..38a2d5dda 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/mod.rs @@ -16,6 +16,7 @@ mod tests { use super::*; use crate::ui_editor::component::image::{ImageComponent, ImageType}; use crate::ui_editor::component::Component; + use crate::ui_editor::component::text::TextComponent; use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode; use crate::ui_editor::layout::control_layout::ControlLayout; use crate::ui_editor::layout::node::Node; @@ -106,6 +107,69 @@ mod tests { assert_eq!(tree.root.id.as_str(), "root-image"); assert!(tree.root_extractable); } + + #[test] + fn construction_attaches_text_mask_to_nearest_unbound_image() { + let image = Component::Image(ImageComponent { + target_graphic: None, + image_type: ImageType::Simple { + preserve_aspect: false, + }, + }); + let text = Component::Text(TextComponent::new("按钮")); + let root = node( + "root", + vec![], + vec![node( + "outer-image", + vec![image.clone()], + vec![node("text", vec![text.clone()], vec![])], + )], + ); + let result = construct_separation_state(&state(root)); + let outer = &result.trees[0].root.children[0]; + assert_eq!(outer.id.as_str(), "outer-image"); + assert_eq!(outer.text_mask_areas.len(), 1); + assert!(outer.children.is_empty()); + + let nested_root = node( + "root", + vec![], + vec![node( + "outer-image", + vec![image.clone()], + vec![node( + "inner-image", + vec![image], + vec![node("text", vec![text], vec![])], + )], + )], + ); + let nested = construct_separation_state(&state(nested_root)); + let inner = &nested.trees[0].root.children[0].children[0]; + assert_eq!(inner.text_mask_areas.len(), 1); + assert!(nested.trees[0].root.children[0].text_mask_areas.is_empty()); + } + + #[test] + fn root_image_receives_text_mask() { + let root = node( + "root-image", + vec![Component::Image(ImageComponent { + target_graphic: None, + image_type: ImageType::Simple { + preserve_aspect: false, + }, + })], + vec![node( + "text", + vec![Component::Text(TextComponent::new("标题"))], + vec![], + )], + ); + let result = construct_separation_state(&state(root)); + assert_eq!(result.trees[0].root.text_mask_areas.len(), 1); + } #[test] fn binding_validation_requires_exact_batch_coverage() { let node = SeparationNode { diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/tree.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/tree.rs index 42e09f701..56328dfef 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/tree.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/separation/tree.rs @@ -3,6 +3,7 @@ use crate::ui_editor::component::{image::ImageComponent, Component}; use crate::ui_editor::layout::node::Node; use crate::ui_editor::state::State; use crate::ui_editor::utils::NodeId; +use std::collections::HashMap; use std::collections::HashSet; fn is_unbound_image(node: &Node) -> bool { @@ -17,6 +18,18 @@ fn is_unbound_image(node: &Node) -> bool { }) } +fn has_image_component(node: &Node) -> bool { + node.components + .iter() + .any(|component| matches!(component, Component::Image(_))) +} + +fn has_text_component(node: &Node) -> bool { + node.components + .iter() + .any(|component| matches!(component, Component::Text(_))) +} + fn node_pixel_rect( node: &Node, parent: &crate::ui_editor::layout::dimension::UIRect, @@ -47,11 +60,12 @@ fn collect_todo_nodes( parent: &crate::ui_editor::layout::dimension::UIRect, ppu: f32, output: &mut Vec, + text_masks: &mut HashMap>, ) { let rect = node.layout.transform.resolve(parent); let mut children = Vec::new(); for child in &node.children { - collect_todo_nodes(child, &rect, ppu, &mut children); + collect_todo_nodes(child, &rect, ppu, &mut children, text_masks); } if is_unbound_image(node) { let (x, y, w, h) = node_pixel_rect(node, parent, ppu); @@ -65,6 +79,7 @@ fn collect_todo_nodes( description: node_description(node), rework_notes: Vec::new(), }, + text_mask_areas: text_masks.remove(&node.id).unwrap_or_default(), children, rework_count: 0, }); @@ -73,6 +88,36 @@ fn collect_todo_nodes( } } +fn collect_text_masks( + node: &Node, + parent: &crate::ui_editor::layout::dimension::UIRect, + ppu: f32, + nearest_image: Option, + output: &mut HashMap>, +) { + let node_is_image = is_unbound_image(node); + let nearest_image = if node_is_image { + Some(node.id.clone()) + } else { + nearest_image + }; + if has_text_component(node) && !has_image_component(node) { + if let Some(image_id) = nearest_image.clone() { + let (x, y, w, h) = node_pixel_rect(node, parent, ppu); + output.entry(image_id).or_default().push(TextMaskArea { + global_pos_x_px: x, + global_pos_y_px: y, + width_px: w, + height_px: h, + }); + } + } + let rect = node.layout.transform.resolve(parent); + for child in &node.children { + collect_text_masks(child, &rect, ppu, nearest_image.clone(), output); + } +} + pub fn construct_separation_state(state: &State) -> SeparationState { let trees = state .ui_trees @@ -83,9 +128,11 @@ pub fn construct_separation_state(state: &State) -> SeparationState { let size = image.pixel_size / ppu; let root_rect = crate::ui_editor::layout::dimension::UIRect::new(nalgebra::Point2::origin(), size); + let mut text_masks = HashMap::new(); + collect_text_masks(&tree.root, &root_rect, ppu, None, &mut text_masks); let mut children = Vec::new(); for child in &tree.root.children { - collect_todo_nodes(child, &root_rect, ppu, &mut children); + collect_todo_nodes(child, &root_rect, ppu, &mut children, &mut text_masks); } let root_extractable = is_unbound_image(&tree.root); if !root_extractable && children.is_empty() { @@ -104,6 +151,7 @@ pub fn construct_separation_state(state: &State) -> SeparationState { description: node_description(&tree.root), rework_notes: Vec::new(), }, + text_mask_areas: text_masks.remove(&tree.root.id).unwrap_or_default(), children, rework_count: 0, },