diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index 151a18cfe..a32df9085 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -197,14 +197,6 @@ use swarm_cli::*; use template_library::*; use user_input::*; use windows::*; -#[tauri::command] -async fn suggest_ui_design_semantic( - project_path: String, - state: ui_editor::state::State, -) -> Result, String> { - ui_editor::commands::suggest_ui_design_semantic_impl(project_path, state).await -} - #[tauri::command] async fn recognize_ui( project_path: String, @@ -2665,7 +2657,6 @@ fn main() { prepare_ui_editor_project_fonts, read_ui_editor_font_bytes, check_ui_editor_font_glyph_coverage, - suggest_ui_design_semantic, recognize_ui, separate_ui, inspect_separation_recovery, diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/merge.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/merge.rs index 36a6e106b..aab189a70 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/merge.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/merge.rs @@ -114,59 +114,6 @@ mod llm_contract { } } -mod priority { - use crate::ui_editor::resource::ui_design_image::UIDesignImage; - use crate::ui_editor::state::State; - use crate::ui_editor::utils::UIDesignImageId; - use std::collections::{HashMap, HashSet}; - - fn ancestor_chain( - image_id: &UIDesignImageId, - images: &HashMap, - ) -> Result, String> { - let mut chain = Vec::new(); - let mut seen = HashSet::new(); - let mut current = image_id.clone(); - loop { - if !seen.insert(current.clone()) { - return Err(format!( - "界面图 slave_to 关系存在循环:{}", - current.as_str() - )); - } - chain.push(current.clone()); - let image = images - .get(¤t) - .ok_or_else(|| format!("界面图 slave_to 引用了缺失界面图:{}", current.as_str()))?; - match &image.metadata.slave_to { - Some(parent) => current = parent.clone(), - None => return Ok(chain), - } - } - } - - pub(super) fn for_state(state: &State) -> Result, String> { - let source_ids = state - .ui_trees - .iter() - .map(|tree| tree.src_ui_design.clone()) - .collect::>(); - let chains = source_ids - .iter() - .map(|source_id| ancestor_chain(source_id, &state.ui_design_images)) - .collect::, _>>()?; - Ok(source_ids - .iter() - .map(|candidate| { - chains - .iter() - .filter(|chain| chain.contains(candidate)) - .count() as u32 - }) - .collect()) - } -} - mod materialize { use super::llm_contract::Node; use crate::ui_editor::layout::children_display_mode::ChildrenDisplayMode; @@ -217,6 +164,7 @@ mod materialize { Ok(()) } + // TODO(ui-merge): 优先级恒 0 时该函数固定取 merged_from 的首个成员;优先级来源待重新设计。 fn highest_priority_member_index(members: &[BuiltNode]) -> Result { if members.is_empty() { return Err("MergedNode.merged_from 不能为空".to_string()); @@ -412,10 +360,8 @@ pub(crate) async fn merge_ui_impl_with_provider( app_log!("ui_merge.error stage=validate_input error={error}"); error })?; - let priorities = priority::for_state(&state).map_err(|error| { - app_log!("ui_merge.error stage=build_priority error={error}"); - error - })?; + // TODO(ui-merge): 输入树优先级来源待重新设计,当前所有输入树等权(恒 0)。 + let priorities = vec![0u32; state.ui_trees.len()]; let trees = llm_contract::input_trees(&state, &priorities).map_err(|error| { app_log!("ui_merge.error stage=build_input error={error}"); error diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/mod.rs index d85432e07..891c64f0f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/mod.rs @@ -2,7 +2,6 @@ pub mod binding; pub mod merge; pub mod recognition; pub mod separation; -pub mod ui_design_suggestion; pub mod utils; pub use binding::BindingDTO; @@ -13,5 +12,3 @@ pub use recognition::RecognitionDTO; pub(crate) use recognition::{recognize_ui_impl, recognize_ui_impl_with_provider}; pub(crate) use separation::separate_ui_impl; pub use separation::{SeparationDTO, SeparationRecoveryDTO}; -pub(crate) use ui_design_suggestion::suggest_ui_design_semantic_impl; -pub use ui_design_suggestion::UIDesignSuggestionTreeNode; diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/recognition.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/recognition.rs index c2f21bfe1..2e23b1aaa 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/recognition.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/recognition.rs @@ -32,7 +32,7 @@ const SYSTEM_PROMPT: &str = r#" 任务: 同时分析同一 UI 系统的全部参考图,建立UI树 -用户会给你一些UI截图(它们从属于同一个UI系统)和对应的元数据, 请用给定的工具描述UI结构 +用户会给你一些UI截图(它们从属于同一个UI系统), 请用给定的工具描述UI结构 识别规则: * 只识别 UI元素. 要区分动态内容, 不要白费力气识别应该由程序生成/绘制的内容.(此类内容应该用一个整体节点+自然语言描述) 除此之外必须完整包含所有元素,结构. @@ -386,12 +386,6 @@ mod tests { fn test_image() -> UIDesignImage { UIDesignImage { - metadata: crate::ui_editor::resource::ui_design_image::UIDesignImageMetadata { - name: "测试图".to_string(), - description: String::new(), - role: None, - slave_to: None, - }, path: "test.png".to_string(), pixel_size: Vector2::new(1000.0, 500.0), pixels_per_unit: typed_floats::tf32::StrictlyPositiveFinite::new(2.0) @@ -548,7 +542,7 @@ mod tests { #[test] fn conversion_stays_in_the_tree_image_coordinate_system() { - let image_id = UIDesignImageId::new("slave").expect("valid image id"); + let image_id = UIDesignImageId::new("second").expect("valid image id"); let image = test_image(); let root_rect = UIRect::new(Point2::origin(), image_layout_size(&image).unwrap()); let converted = @@ -591,56 +585,29 @@ mod tests { #[test] fn tree_validation_requires_exactly_one_tree_per_context_image() { let page = UIDesignImageId::new("page").expect("valid image id"); - let slave = UIDesignImageId::new("slave").expect("valid image id"); + let second = UIDesignImageId::new("second").expect("valid image id"); let tree = |id: UIDesignImageId| RecognitionTree { src_ui_design_image_id: id, root: test_node(), }; assert!(validate_tree_image_ids( - &[tree(page.clone()), tree(slave.clone())], - &[page.clone(), slave.clone()], + &[tree(page.clone()), tree(second.clone())], + &[page.clone(), second.clone()], ) .is_ok()); assert!( - validate_tree_image_ids(&[tree(page.clone())], &[page.clone(), slave.clone()]).is_err() - ); - assert!( - validate_tree_image_ids(&[tree(page.clone()), tree(page.clone())], &[page, slave],) + validate_tree_image_ids(&[tree(page.clone())], &[page.clone(), second.clone()]) .is_err() ); + assert!(validate_tree_image_ids( + &[tree(page.clone()), tree(page.clone())], + &[page, second], + ) + .is_err()); } } -fn recognition_root_image_ids(state: &State) -> Vec { - state - .ui_design_images - .iter() - .filter_map(|(id, image)| { - let is_page = image.metadata.role - == Some(crate::ui_editor::resource::ui_design_image::UIDesignImageRole::Page); - let is_direct_slave_of_page = image.metadata.slave_to.as_ref().is_some_and(|parent| { - state - .ui_design_images - .get(parent) - .and_then(|parent_image| parent_image.metadata.role) - == Some(crate::ui_editor::resource::ui_design_image::UIDesignImageRole::Page) - }); - (is_page || !is_direct_slave_of_page).then(|| id.clone()) - }) - .collect() -} - -fn slave_image_ids(state: &State, root_id: &UIDesignImageId) -> Vec { - state - .ui_design_images - .iter() - .filter_map(|(id, image)| { - (image.metadata.slave_to.as_ref() == Some(root_id)).then(|| id.clone()) - }) - .collect() -} - pub(crate) async fn recognize_ui_impl_with_provider( project_path: String, state: State, @@ -657,11 +624,7 @@ pub(crate) async fn recognize_ui_impl_with_provider( ); return Err("界面图最多 4 张".to_string()); } - let root_ids = recognition_root_image_ids(&state); - if root_ids.is_empty() { - app_log!("ui_recognition.error stage=validate reason=no_root_image"); - return Err("至少需要一张可作为识别上下文根的界面图".to_string()); - } + let root_ids = state.ui_design_images.keys().cloned().collect::>(); let (llm, client) = if provider_identity.is_none() { let llm = load_game_creator_app_config() .map_err(|error| { @@ -685,12 +648,9 @@ pub(crate) async fn recognize_ui_impl_with_provider( let root = Path::new(project_path.trim()); let mut ui_trees = Vec::with_capacity(root_ids.len()); for root_id in root_ids { - let mut context_ids = vec![root_id.clone()]; - context_ids.extend(slave_image_ids(&state, &root_id)); - // Page 仍可把直接 slave 参考图放在同一识别上下文;没有 Page - // 归属关系的其它界面图各自作为独立上下文根。 + let context_ids = vec![root_id.clone()]; let mut parts = Vec::with_capacity(context_ids.len() * 2); - for (index, context_id) in context_ids.iter().enumerate() { + for context_id in context_ids.iter() { let image = state .ui_design_images .get(context_id) @@ -716,8 +676,7 @@ pub(crate) async fn recognize_ui_impl_with_provider( })?; parts.push(LlmMessageContentPart::InputText { text: format!( - "{} id={} pixel_size={:?}", - if index == 0 { "ROOT" } else { "SLAVE" }, + "ROOT id={} pixel_size={:?}", context_id.as_str(), image.pixel_size ), 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 dc5376acb..595dee76e 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 @@ -60,12 +60,6 @@ mod tests { ui_design_images: HashMap::from([( image_id, UIDesignImage { - metadata: crate::ui_editor::resource::ui_design_image::UIDesignImageMetadata { - name: "page".to_string(), - description: String::new(), - role: None, - slave_to: None, - }, path: "page.png".to_string(), pixel_size: Vector2::new(100.0, 100.0), pixels_per_unit: StrictlyPositiveFinite::new(1.0).unwrap(), diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/ui_design_suggestion.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/ui_design_suggestion.rs deleted file mode 100644 index b32c6f448..000000000 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/commands/ui_design_suggestion.rs +++ /dev/null @@ -1,349 +0,0 @@ -use crate::config::build_game_creator_llm_client_from_llm_config; -use crate::config::load_game_creator_app_config; -use crate::ui_editor::commands::utils::{ - parse_limited_llm_tool_arguments, read_ui_reference_image_data_url, request_ui_editor_llm, - required_tool_call_arguments, strict_json_schema, -}; -use crate::ui_editor::resource::ui_design_image::UIDesignImageRole; -use crate::ui_editor::state::State; -use crate::ui_editor::utils::UIDesignImageId; -use platform_llm::{ - LlmFunctionTool, LlmMessage, LlmMessageContentPart, LlmRunRequest, LlmToolChoice, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::collections::HashSet; -use std::path::Path; -use ts_rs::TS; - -const SYSTEM_PROMPT: &str = r#" -请识别这些 UI 参考图的界面语义,并调用 suggest_ui_design_semantics 工具返回结果。 -不要在工具调用外输出 JSON、Markdown、代码围栏、注释、额外字段或解释文字。 - -字段含义: -- id:必填,必须逐字匹配当前输入参考图的 id。它标识“这条建议属于哪张图”,不是新 ID,不能为 null。 -- name:要写入该图片 metadata 的简短、可读名称; -- description:要写入该图片 metadata 的简短语义描述,例如“带底部导航的主游戏页面”。 -- role:要写入该图片 metadata 的界面角色。 - Page 表示完整主页面, - Section 表示同一主页面中的子界面或页签, - Modal/Drawer/Popover 表示浮层或局部覆盖界面, - State 表示同一界面的状态变体, - Scrolled 表示滚动或分页后的内容, - Detail 表示局部详情或补充证据。 -- children:该节点直接包含的子界面图。 - -- 根节点必须是 Page,子节点不能是 Page - -所有字段都必须出现,禁止省略字段、使用 null 或返回空字符串。每张参考图最多出现一次;id 必须逐字匹配输入图片 id。 -以下 UI 设计图中的 metadata 部分信息已确定,请根据已有信息补全语义. -"#; -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS, JsonSchema)] -#[schemars(deny_unknown_fields)] -#[serde(deny_unknown_fields)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))] -pub struct UIDesignSuggestionTreeNode { - pub id: UIDesignImageId, - pub name: String, - pub description: String, - pub role: UIDesignImageRole, - pub children: Vec, -} - -#[derive(Clone, Debug, Deserialize, JsonSchema)] -#[schemars(deny_unknown_fields)] -#[serde(deny_unknown_fields)] -struct UIDesignSuggestion { - ui_designs: Vec, -} - -fn ui_design_suggestion_json_schema() -> Result { - strict_json_schema::() -} - -const MAX_REFERENCES: usize = 4; -const MAX_SUGGESTION_TREE_DEPTH: usize = 4; - -fn validate_suggestion_response_shape(value: &serde_json::Value) -> Result<(), String> { - let roots = value - .get("ui_designs") - .and_then(serde_json::Value::as_array) - .ok_or_else(|| "UI 语义建议工具参数缺少 ui_designs 数组".to_string())?; - let mut stack = roots.iter().map(|node| (node, 1usize)).collect::>(); - let mut node_count = 0usize; - while let Some((node, depth)) = stack.pop() { - if depth > MAX_SUGGESTION_TREE_DEPTH { - return Err(format!( - "UI 语义建议树最大深度不能超过 {MAX_SUGGESTION_TREE_DEPTH}" - )); - } - node_count += 1; - if node_count > MAX_REFERENCES { - return Err(format!("UI 语义建议最多包含 {MAX_REFERENCES} 个节点")); - } - let children = node - .get("children") - .and_then(serde_json::Value::as_array) - .ok_or_else(|| "UI 语义建议节点缺少 children 数组".to_string())?; - stack.extend(children.iter().map(|child| (child, depth + 1))); - } - Ok(()) -} - -fn validate_suggestions( - suggestions: &[UIDesignSuggestionTreeNode], - image_ids: &HashSet, -) -> Result<(), String> { - let mut seen = HashSet::new(); - - fn visit( - nodes: &[UIDesignSuggestionTreeNode], - is_root: bool, - image_ids: &HashSet, - seen: &mut HashSet, - ) -> Result<(), String> { - for node in nodes { - if !image_ids.contains(&node.id) { - return Err("LLM 返回了未知界面图 ID".to_string()); - } - if !seen.insert(node.id.clone()) { - return Err("LLM 返回了重复界面图建议".to_string()); - } - if node.name.trim().is_empty() || node.description.trim().is_empty() { - return Err("界面图语义字段不能是空字符串".to_string()); - } - if is_root { - if node.role != UIDesignImageRole::Page { - return Err("界面图树根节点必须是 Page".to_string()); - } - } else if node.role == UIDesignImageRole::Page { - return Err("Page 不能作为其它界面图的子节点".to_string()); - } - visit(&node.children, false, image_ids, seen)?; - } - Ok(()) - } - - visit(suggestions, true, image_ids, &mut seen)?; - if seen != *image_ids { - return Err("LLM 未为每张界面图返回语义建议".to_string()); - } - Ok(()) -} - -#[tauri::command] -pub(crate) async fn suggest_ui_design_semantic_impl( - project_path: String, - state: State, -) -> Result, String> { - if state.ui_design_images.is_empty() { - app_log!("ui_design_suggestion.error stage=validate reason=no_images"); - return Err("请先导入界面图".to_string()); - } - if state.ui_design_images.len() > MAX_REFERENCES { - app_log!( - "ui_design_suggestion.error stage=validate reason=too_many_images count={}", - state.ui_design_images.len() - ); - return Err("界面图最多 4 张".to_string()); - } - let root = Path::new(project_path.trim()); - let mut parts = Vec::new(); - let mut ids = HashSet::new(); - for (id, image) in &state.ui_design_images { - ids.insert(id.clone()); - let absolute = - crate::project::resolve_local_project_path(root, &image.path).map_err(|error| { - app_log!( - "ui_design_suggestion.error stage=resolve_image id={} error={error}", - id.as_str() - ); - error - })?; - let image_url = read_ui_reference_image_data_url(absolute) - .await - .map_err(|error| { - app_log!( - "ui_design_suggestion.error stage=read_image id={} error={error}", - id.as_str() - ); - error - })?; - parts.push(LlmMessageContentPart::InputText { - text: format!("REFERENCE id:{} metadata:{}", id.as_str(), image.metadata,), - }); - parts.push(LlmMessageContentPart::InputImage { image_url }); - } - let llm = load_game_creator_app_config() - .map_err(|error| { - eprintln!("ui_design_suggestion.error stage=build_client error={error}"); - error - })? - .llm; - let client = build_game_creator_llm_client_from_llm_config(&llm, "llm").map_err(|error| { - app_log!("ui_design_suggestion.error stage=build_client error={error}"); - error - })?; - let schema = ui_design_suggestion_json_schema().map_err(|error| { - app_log!("ui_design_suggestion.error stage=build_schema error={error}"); - error - })?; - let tool = LlmFunctionTool::new( - "suggest_ui_design_semantics", - "为所有 UI 参考图补全界面语义 metadata", - schema, - ) - .with_strict(true); - let response = request_ui_editor_llm( - &client, - &llm, - LlmRunRequest::new(vec![ - LlmMessage::system(SYSTEM_PROMPT), - LlmMessage::user_multimodal(parts), - ]) - .with_function_tools(vec![tool]) - .with_tool_choice(LlmToolChoice::Required), - ) - .await - .map_err(|error| { - app_log!("ui_design_suggestion.error stage=llm_request error={error}"); - format!("UI 参考图语义识别失败:{error}") - })?; - app_log!( - "ui_design_suggestion.llm_output text_present={} tool_call_count={}", - !response.text.trim().is_empty(), - response.tool_calls.len() - ); - let arguments = required_tool_call_arguments(&response, "suggest_ui_design_semantics") - .map_err(|error| { - app_log!( - "ui_design_suggestion.error stage=parse_tool_call reason=missing_tool_call error={error}" - ); - "LLM 响应无效(详情:未返回 suggest_ui_design_semantics 工具调用)".to_string() - })?; - let arguments = parse_limited_llm_tool_arguments(arguments).map_err(|error| { - app_log!("ui_design_suggestion.error stage=parse_arguments error={error}"); - format!("LLM 响应无效(详情:UI 语义建议工具参数无效:{error})") - })?; - validate_suggestion_response_shape(&arguments).map_err(|error| { - app_log!("ui_design_suggestion.error stage=validate_arguments error={error}"); - format!("LLM 响应无效(详情:{error})") - })?; - let suggestions = serde_json::from_value::(arguments) - .map_err(|error| { - app_log!("ui_design_suggestion.error stage=parse_arguments error={error}"); - format!("LLM 响应无效(详情:UI 语义建议工具参数无效:{error})") - })? - .ui_designs; - validate_suggestions(&suggestions, &ids).map_err(|error| { - app_log!("ui_design_suggestion.error stage=validate_result error={error}"); - format!("LLM 响应无效(详情:{error})") - })?; - Ok(suggestions) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn id(value: &str) -> UIDesignImageId { - UIDesignImageId::new(value).expect("valid image id") - } - - fn node( - value: &str, - role: UIDesignImageRole, - children: Vec, - ) -> UIDesignSuggestionTreeNode { - UIDesignSuggestionTreeNode { - id: id(value), - name: value.to_string(), - description: format!("{value} description"), - role, - children, - } - } - - fn image_ids(values: &[&str]) -> HashSet { - values.iter().map(|value| id(value)).collect() - } - - #[test] - fn validates_page_tree() { - let suggestions = vec![node( - "page", - UIDesignImageRole::Page, - vec![node("section", UIDesignImageRole::Section, Vec::new())], - )]; - assert!(validate_suggestions(&suggestions, &image_ids(&["page", "section"])).is_ok()); - } - - #[test] - fn suggestion_shape_rejects_more_than_four_nodes_or_four_levels() { - let oversized = (0..=MAX_REFERENCES) - .map(|_| serde_json::json!({"children": []})) - .collect::>(); - assert!(validate_suggestion_response_shape(&serde_json::json!({ - "ui_designs": oversized - })) - .is_err()); - - let mut nested = serde_json::json!({"children": []}); - for _ in 0..MAX_SUGGESTION_TREE_DEPTH { - nested = serde_json::json!({"children": [nested]}); - } - assert!(validate_suggestion_response_shape(&serde_json::json!({ - "ui_designs": [nested] - })) - .is_err()); - } - - #[test] - fn rejects_duplicate_and_unknown_ids() { - let duplicate = vec![ - node( - "page", - UIDesignImageRole::Page, - vec![node("section", UIDesignImageRole::Section, Vec::new())], - ), - node("page", UIDesignImageRole::Page, Vec::new()), - ]; - assert!(validate_suggestions(&duplicate, &image_ids(&["page", "section"])).is_err()); - - let unknown = vec![node("missing", UIDesignImageRole::Page, Vec::new())]; - assert!(validate_suggestions(&unknown, &image_ids(&["page"])).is_err()); - } - - #[test] - fn rejects_invalid_root_roles_and_nested_pages() { - let non_page_root = vec![node("section", UIDesignImageRole::Section, Vec::new())]; - assert!(validate_suggestions(&non_page_root, &image_ids(&["section"])).is_err()); - - let nested_page = vec![node( - "page", - UIDesignImageRole::Page, - vec![node("nested", UIDesignImageRole::Page, Vec::new())], - )]; - assert!(validate_suggestions(&nested_page, &image_ids(&["page", "nested"])).is_err()); - } - - #[test] - fn strict_schema_requires_tree_fields() { - let schema = ui_design_suggestion_json_schema().expect("schema"); - assert_eq!(schema["required"], serde_json::json!(["ui_designs"])); - let node_schema = &schema["properties"]["ui_designs"]["items"]; - let required = node_schema["required"] - .as_array() - .expect("node required") - .iter() - .filter_map(serde_json::Value::as_str) - .collect::>(); - assert_eq!( - required, - ["id", "name", "description", "role", "children"] - .into_iter() - .collect() - ); - assert_eq!(node_schema["additionalProperties"], false); - } -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/html_renderer/mod.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/html_renderer/mod.rs index e908a3117..03a5ba6ff 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/html_renderer/mod.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/html_renderer/mod.rs @@ -84,8 +84,6 @@ pub(crate) fn render_ui_design_state_js( "genarrative-ui-tree", json!({ "srcUiDesign": tree.src_ui_design.as_str(), - "name": image.metadata.name, - "description": image.metadata.description, }), ) .into_string(); diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs index da9d1250b..454fbc9f9 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/persistence.rs @@ -2,9 +2,7 @@ use crate::ui_editor::component::text::FontSource; use crate::ui_editor::component::Component; use crate::ui_editor::html_renderer::render_ui_design_state_js; use crate::ui_editor::layout::node::Node; -use crate::ui_editor::resource::ui_design_image::{ - UIDesignImage, UIDesignImageMetadata, UIDesignImageRole, -}; +use crate::ui_editor::resource::ui_design_image::UIDesignImage; use crate::ui_editor::state::State; use crate::ui_editor::utils::UIDesignImageId; use crate::*; @@ -139,12 +137,6 @@ pub(crate) fn initialize_ui_design_state_with_source_image_at( document.state.ui_design_images.insert( source_image_id, UIDesignImage { - metadata: UIDesignImageMetadata { - name: "游戏界面原型".to_string(), - description: "由画布 UI 原型桥接载入".to_string(), - role: Some(UIDesignImageRole::Page), - slave_to: None, - }, path: source_image_path, pixel_size: Vector2::new(pixel_size.0 as f32, pixel_size.1 as f32), pixels_per_unit, @@ -614,16 +606,7 @@ fn validate_state(state: &State) -> Result<(), String> { { return Err("界面图像素尺寸必须为正有限数值".to_string()); } - if image - .metadata - .slave_to - .as_ref() - .is_some_and(|owner| !state.ui_design_images.contains_key(owner)) - { - return Err("界面图 slaveTo 引用了不存在的界面图".to_string()); - } } - validate_slave_to_acyclic(state)?; for (id, sprite) in &state.sprite_assets { validate_id(id.as_str(), "独立素材 ID")?; if sprite.asset_id != *id { @@ -649,23 +632,6 @@ fn validate_state(state: &State) -> Result<(), String> { Ok(()) } -fn validate_slave_to_acyclic(state: &State) -> Result<(), String> { - for start in state.ui_design_images.keys() { - let mut current = Some(start); - let mut visited = HashSet::new(); - while let Some(id) = current { - if !visited.insert(id) { - return Err("界面图 slaveTo 不能形成循环".to_string()); - } - current = state - .ui_design_images - .get(id) - .and_then(|image| image.metadata.slave_to.as_ref()); - } - } - Ok(()) -} - fn validate_node( node: &Node, state: &State, @@ -1050,12 +1016,6 @@ mod tests { "ui_trees": [], "ui_design_images": { "page": { - "metadata": { - "name": "主界面", - "description": "", - "role": "Page", - "slave_to": null - }, "path": path, "pixel_size": [1280.0, 720.0], "pixels_per_unit": 1.0 @@ -1136,12 +1096,6 @@ mod tests { }], "ui_design_images": { "page": { - "metadata": { - "name": "主界面", - "description": "", - "role": "Page", - "slave_to": null - }, "path": "assets/page.png", "pixel_size": [1280.0, 720.0], "pixels_per_unit": 1.0 @@ -1319,12 +1273,6 @@ mod tests { }], "ui_design_images": { "page": { - "metadata": { - "name": "主界面", - "description": "", - "role": "Page", - "slave_to": null - }, "path": "missing.png", "pixel_size": [1280.0, 720.0], "pixels_per_unit": 1.0 @@ -1341,45 +1289,6 @@ mod tests { ); } - #[test] - fn rejects_cyclic_slave_to_relationships() { - let state: State = serde_json::from_value(serde_json::json!({ - "ui_trees": [], - "ui_design_images": { - "page": { - "metadata": { - "name": "主界面", - "description": "", - "role": "Page", - "slave_to": "section" - }, - "path": "page.png", - "pixel_size": [1280.0, 720.0], - "pixels_per_unit": 1.0 - }, - "section": { - "metadata": { - "name": "子页面", - "description": "", - "role": "Section", - "slave_to": "page" - }, - "path": "section.png", - "pixel_size": [1280.0, 720.0], - "pixels_per_unit": 1.0 - } - }, - "sprite_assets": {}, - "font_assets": {} - })) - .expect("deserialize cyclic state"); - - assert_eq!( - validate_state(&state), - Err("界面图 slaveTo 不能形成循环".to_string()) - ); - } - #[test] fn recovers_last_valid_state_when_primary_is_corrupt() { let (directory, asset_id) = fixture(); @@ -1554,12 +1463,6 @@ mod tests { }], "ui_design_images": { "page": { - "metadata": { - "name": "主界面", - "description": "", - "role": "Page", - "slave_to": null - }, "path": "assets/page.png", "pixel_size": [1280.0, 720.0], "pixels_per_unit": 1.0 diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource/ui_design_image.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource/ui_design_image.rs index d1f3b7714..7c18fa7c2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource/ui_design_image.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/resource/ui_design_image.rs @@ -1,48 +1,14 @@ -use crate::ui_editor::utils::UIDesignImageId; use nalgebra::Vector2; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use std::fmt::{Display, Formatter}; use ts_rs::TS; use typed_floats::tf32::StrictlyPositiveFinite; -#[derive(Clone, Copy, Debug, Deserialize, Eq, JsonSchema, PartialEq, Serialize, TS)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))] -pub enum UIDesignImageRole { - Page, - Section, - Modal, - Drawer, - Popover, - State, - Scrolled, - Detail, -} - #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)] #[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))] pub struct UIDesignImage { - pub(crate) metadata: UIDesignImageMetadata, pub(crate) path: String, #[ts(as = "[f32; 2]")] pub(crate) pixel_size: Vector2, #[ts(as = "f32")] pub(crate) pixels_per_unit: StrictlyPositiveFinite, } -#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)] -#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/features/ui-editor/types/"))] -pub struct UIDesignImageMetadata { - pub(crate) name: String, - pub(crate) description: String, - pub(crate) role: Option, - pub(crate) slave_to: Option, -} -impl Display for UIDesignImageMetadata { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "name: {}, description: {}, role: {:?}, slave_to: {:?}", - self.name, self.description, self.role, self.slave_to - ) - } -} diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs index e827ea75d..0c71cce22 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/workflow.rs @@ -11,9 +11,7 @@ use crate::ui_editor::persistence::{ }; use crate::ui_editor::resource::font::FontAsset; use crate::ui_editor::resource::sprite::{SpriteAsset, SpriteAssetMetadata, SpriteBorder}; -use crate::ui_editor::resource::ui_design_image::{ - UIDesignImage, UIDesignImageMetadata, UIDesignImageRole, -}; +use crate::ui_editor::resource::ui_design_image::UIDesignImage; use crate::ui_editor::utils::{SpriteAssetId, UIDesignImageId}; use crate::*; use image::GenericImageView as _; @@ -801,7 +799,7 @@ fn ensure_page_ui_resource( })?; let image_id = UIDesignImageId::new(page.page_id.clone()) .map_err(|error| format!("页面 image ID 无效:{error}"))?; - let expected_image = workflow_design_image(root, page, design_asset)?; + let expected_image = workflow_design_image(root, design_asset)?; let mut state = snapshot.state; match state.ui_design_images.get(&image_id) { Some(current) if current != &expected_image => { @@ -1235,7 +1233,6 @@ fn workflow_stage_rank(kind: &str) -> Option { fn workflow_design_image( root: &Path, - page: &UiWorkflowPageInput, design_asset: &GameCreationAppAssetManifestEntry, ) -> Result { let absolute = resolve_local_project_path(root, &design_asset.local_path)?; @@ -1248,12 +1245,6 @@ fn workflow_design_image( return Err("页面设计图尺寸无效".to_string()); } Ok(UIDesignImage { - metadata: UIDesignImageMetadata { - name: page.title.trim().to_string(), - description: page.description.trim().to_string(), - role: Some(UIDesignImageRole::Page), - slave_to: None, - }, path: design_asset.local_path.clone(), pixel_size: Vector2::new(width as f32, height as f32), pixels_per_unit: StrictlyPositiveFinite::new(1.0)