WIP: UI编辑器自动分图层切图标 #304

Draft
k88936 wants to merge 100 commits from feat/ui-editor-auto-seperation into master
2 changed files with 114 additions and 2 deletions
Showing only changes of commit 56f8c9a6a1 - Show all commits
@@ -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 {
@@ -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<SeparationNode>,
text_masks: &mut HashMap<NodeId, Vec<TextMaskArea>>,
) {
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<NodeId>,
output: &mut HashMap<NodeId, Vec<TextMaskArea>>,
) {
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,
},