diff --git a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/layout/node.rs b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/layout/node.rs index ae93e2bd2..17ecbd346 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/ui_editor/layout/node.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/ui_editor/layout/node.rs @@ -15,6 +15,8 @@ pub struct Node { pub component: Option, pub children_display_mode: ChildrenDisplayMode, pub children: Vec, + // 偏移量是后加的字段:历史设计文档没有它,默认回落到零偏移,保证旧文档仍可读。 + #[serde(default)] pub offset: NodeOffset, } 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 6c64ebdf0..a95d17803 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 @@ -832,6 +832,7 @@ fn validate_id(value: &str, label: &str) -> Result<(), String> { #[cfg(test)] mod tests { use super::*; + use crate::ui_editor::layout::offset::NodeOffset; const PROJECT_ID: &str = "ui-design-persistence-project"; @@ -1444,6 +1445,43 @@ mod tests { .is_err()); } + /// 历史设计文档没有节点 `offset` 字段:缺字段必须按零偏移解析,不能判成缺字段损坏。 + #[test] + fn legacy_nodes_without_offset_field_fall_back_to_zero_offset() { + let mut value = serde_json::to_value(state_with_sprite_and_dragged_transform()) + .expect("serialize dragged UI design state"); + strip_offset_fields(&mut value); + let state: State = serde_json::from_value(value).expect("节点缺 offset 时仍可解析"); + assert_eq!( + state.ui_trees[0].root.offset, + NodeOffset::default(), + "根节点缺 offset 时回落零偏移" + ); + assert_eq!( + state.ui_trees[0].root.children[0].offset, + NodeOffset::default(), + "子节点缺 offset 时回落零偏移" + ); + } + + /// 递归删除夹具里所有节点级 `offset` 字段,模拟该字段引入前落盘的文档。 + fn strip_offset_fields(value: &mut serde_json::Value) { + match value { + serde_json::Value::Object(fields) => { + fields.remove("offset"); + for field in fields.values_mut() { + strip_offset_fields(field); + } + } + serde_json::Value::Array(items) => { + for item in items { + strip_offset_fields(item); + } + } + _ => {} + } + } + #[test] fn rejects_text_component_with_missing_font_asset() { let (directory, asset_id) = fixture();