From d00fbd29ccdba1ed209495f8ee1cc9ed542d5a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Tue, 18 Aug 2026 16:31:59 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A6=81=E6=AD=A2=20UI=20Transform=20=E5=80=92?= =?UTF-8?q?=E7=BD=AE=E9=94=9A=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 持久化 State 校验拒绝 anchor_min 大于 anchor_max tf2css 在转换边界拒绝倒置 anchor 并更新回归测试 --- .../src-tauri/src/ui_editor/persistence.rs | 68 +++++++++++++++++++ .../ui-editor/utils/transform/tf2css.test.tsx | 21 ++++-- .../ui-editor/utils/transform/tf2css.ts | 6 ++ 3 files changed, 90 insertions(+), 5 deletions(-) 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 4992ea849..81951dbb3 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 @@ -536,6 +536,16 @@ fn validate_node( { return Err("UI 节点 Transform 必须是有限数值".to_string()); } + if node + .layout + .transform + .anchor_min + .iter() + .zip(node.layout.transform.anchor_max.iter()) + .any(|(min, max)| min > max) + { + return Err("UI 节点 Transform 的 anchor 必须按 min 到 max 排序".to_string()); + } if !node .layout .custom_minimum_size @@ -882,6 +892,64 @@ mod tests { assert_eq!(loaded.state, state); } + #[test] + fn rejects_inverted_transform_anchors() { + let state: State = serde_json::from_value(serde_json::json!({ + "ui_trees": [{ + "src_ui_design": "page", + "root": { + "id": "root", + "layout": { + "transform": { + "anchor_min": [0.8, 0.0], + "anchor_max": [0.2, 1.0], + "offset_min": [0.0, 0.0], + "offset_max": [0.0, 0.0] + }, + "custom_minimum_size": [0.0, 0.0], + "size_flags_horizontal": 1, + "size_flags_vertical": 1, + "size_flags_stretch_ratio": 1.0, + "container": "None" + }, + "metadata": { + "name": "根节点", + "description": "", + "layout_status": "Pending", + "components_status": "Pending", + "allow_llm_edit_layout": true, + "allow_llm_edit_component": true, + "source": "System" + }, + "components": [], + "children_display_mode": "Stack", + "children": [] + } + }], + "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 + } + }, + "sprite_assets": {}, + "font_assets": {} + })) + .expect("deserialize state with inverted anchors"); + + assert_eq!( + validate_state(&state), + Err("UI 节点 Transform 的 anchor 必须按 min 到 max 排序".to_string()) + ); + } + #[test] fn recovers_last_valid_state_when_primary_is_corrupt() { let (directory, asset_id) = fixture(); diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/utils/transform/tf2css.test.tsx b/apps/ai-game-creator-shell/src/features/ui-editor/utils/transform/tf2css.test.tsx index 131384c4b..0870bfece 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/utils/transform/tf2css.test.tsx +++ b/apps/ai-game-creator-shell/src/features/ui-editor/utils/transform/tf2css.test.tsx @@ -105,20 +105,20 @@ describe('tf2css', () => { }); }); - it('preserves out-of-range anchors and normalizes negative zero', () => { + it('preserves out-of-range ordered anchors and normalizes negative zero', () => { expect( tf2css({ - anchor_min: [-0.25, 1.25], - anchor_max: [1.5, -0.5], + anchor_min: [-0.25, -0.5], + anchor_max: [1.5, 1.25], offset_min: [-0, -0], offset_max: [-0, -0], }), ).toEqual({ position: 'absolute', left: '-25%', - top: '125%', + top: '-50%', right: '-50%', - bottom: '150%', + bottom: '-25%', }); expect(tf2css(STRETCH_TRANSFORM)).toEqual({ @@ -157,4 +157,15 @@ describe('tf2css', () => { ), ); }); + + it('rejects inverted anchors', () => { + expect(() => + tf2css({ + anchor_min: [0.8, 0], + anchor_max: [0.2, 1], + offset_min: [0, 0], + offset_max: [0, 0], + }), + ).toThrowError('Transform anchors must be ordered'); + }); }); diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/utils/transform/tf2css.ts b/apps/ai-game-creator-shell/src/features/ui-editor/utils/transform/tf2css.ts index 2114b18f0..adee581a1 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/utils/transform/tf2css.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/utils/transform/tf2css.ts @@ -69,6 +69,12 @@ function toCssLength(percentage: number, pixelOffset: number): string { */ export function tf2css(transform: Transform): CSSProperties { assertFiniteTransform(transform); + if ( + transform.anchor_min[0] > transform.anchor_max[0] || + transform.anchor_min[1] > transform.anchor_max[1] + ) { + throw new RangeError('Transform anchors must be ordered'); + } return { position: 'absolute',