禁止 UI Transform 倒置锚点
持久化 State 校验拒绝 anchor_min 大于 anchor_max tf2css 在转换边界拒绝倒置 anchor 并更新回归测试
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user