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 e143ab9c1..911c874b2 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 @@ -221,13 +221,16 @@ fn convert_node( let source_min_px = Vector2::new(source.global_pos_x_px as f32, source.global_pos_y_px as f32); let source_size_node_px = Vector2::new(source.width_px as f32, source.height_px as f32); let source_max_px = source_min_px + source_size_node_px; - let mut blocked = source.width_px == 0 || source.height_px == 0; + let mut blocked_reasons = Vec::new(); + if source.width_px == 0 || source.height_px == 0 { + blocked_reasons.push("节点宽度或高度为 0"); + } if source_min_px.x < 0.0 || source_min_px.y < 0.0 || source_max_px.x > image_size_px.x || source_max_px.y > image_size_px.y { - blocked = true; + blocked_reasons.push("节点像素范围超出界面图边界"); } let ppu = image.pixels_per_unit.get(); if !ppu.is_finite() || ppu <= 0.0 { @@ -245,23 +248,23 @@ fn convert_node( || target_rect.max().x > parent_rect.max().x || target_rect.max().y > parent_rect.max().y { - blocked = true; + blocked_reasons.push("节点布局范围超出父节点边界"); } if anchor_ranges(&source.local_anchor).is_err() { - blocked = true; + blocked_reasons.push("锚点范围无效"); } if source.name.trim().is_empty() { - blocked = true; + blocked_reasons.push("节点名称为空"); } let mut transform = Transform::new(anchor_min, anchor_max, Vector2::zeros(), Vector2::zeros()); transform.set_resolved_rect(&parent_rect, target_rect); - let status = if blocked { - StageStatus::Blocked - } else { + let status = if blocked_reasons.is_empty() { match &source.confidence { Confidence::Confident => StageStatus::Passed, Confidence::UnSure(reason) => StageStatus::NeedReview(reason.clone()), } + } else { + StageStatus::Blocked(blocked_reasons.join(";")) }; let children = source .children @@ -440,6 +443,33 @@ mod tests { assert_eq!(converted.metadata.components_status, StageStatus::Passed); } + #[test] + fn conversion_records_every_blocking_reason_for_the_inspector() { + let image_id = UIDesignImageId::new("page").expect("valid image id"); + let image = test_image(); + let root_rect = UIRect::new(Point2::origin(), image_layout_size(&image).unwrap()); + let mut node = test_node(); + node.width_px = 0; + node.global_pos_x_px = 1001; + node.local_anchor = Anchor::CustomMinMax(CustomMinMaxAnchor { + min_x: 0.9, + min_y: 0.0, + max_x: 0.1, + max_y: 1.0, + }); + node.name = " ".to_string(); + + let converted = convert_node(&node, &image_id, &image, root_rect) + .expect("blocking recognition node still materializes"); + assert_eq!( + converted.metadata.layout_status, + StageStatus::Blocked( + "节点宽度或高度为 0;节点像素范围超出界面图边界;节点布局范围超出父节点边界;锚点范围无效;节点名称为空" + .to_string(), + ), + ); + } + #[test] fn tree_validation_requires_exactly_one_tree_per_context_image() { let page = UIDesignImageId::new("page").expect("valid image id"); 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 dbddf62cd..ad6a87984 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 @@ -30,7 +30,7 @@ pub enum StageStatus { Pending, Passed, NeedReview(String), // reason inside - Blocked, + Blocked(String), // reason inside } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)] diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/bindingOverview.ts b/apps/ai-game-creator-shell/src/features/ui-editor/bindingOverview.ts index 665e54548..e2c7e0cae 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/bindingOverview.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/bindingOverview.ts @@ -1,5 +1,6 @@ import { collectUiTreeNodeTargets, + isBlocked, isNeedReview, type UiTreeNodeTarget, } from './stageStatusOverview'; @@ -84,8 +85,8 @@ export function getBindingOverview( for (const { node } of collectUiTreeNodeTargets(uiTrees)) { const status = node.metadata.components_status; - if (status === 'Blocked') overview.blocked += 1; - if (status === 'Blocked' || isNeedReview(status)) { + if (isBlocked(status)) overview.blocked += 1; + if (isBlocked(status) || isNeedReview(status)) { overview.needsAttention += 1; } for (const component of node.components) { @@ -104,9 +105,9 @@ export function nodeHasPendingBinding(target: UiTreeNodeTarget): boolean { export function nodeNeedsComponentReview(target: UiTreeNodeTarget): boolean { const status = target.node.metadata.components_status; - return status === 'Blocked' || isNeedReview(status); + return isBlocked(status) || isNeedReview(status); } export function nodeHasBlockedComponents(target: UiTreeNodeTarget): boolean { - return target.node.metadata.components_status === 'Blocked'; + return isBlocked(target.node.metadata.components_status); } diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/stageStatusOverview.ts b/apps/ai-game-creator-shell/src/features/ui-editor/stageStatusOverview.ts index 1fd090f7d..938432b78 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/stageStatusOverview.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/stageStatusOverview.ts @@ -25,6 +25,10 @@ export function isNeedReview(status: StageStatus): boolean { return typeof status === 'object' && 'NeedReview' in status; } +export function isBlocked(status: StageStatus): boolean { + return typeof status === 'object' && 'Blocked' in status; +} + export function collectUiTreeNodeTargets( uiTrees: UITree[], ): UiTreeNodeTarget[] { @@ -54,8 +58,8 @@ export function getStageStatusOverview( const status = node.metadata[field]; overview.total += 1; if (status === 'Passed') overview.passed += 1; - if (status === 'Blocked') overview.blocked += 1; - if (status === 'Blocked' || isNeedReview(status)) { + if (isBlocked(status)) overview.blocked += 1; + if (isBlocked(status) || isNeedReview(status)) { overview.needsAttention += 1; } } diff --git a/apps/ai-game-creator-shell/src/features/ui-editor/types/StageStatus.ts b/apps/ai-game-creator-shell/src/features/ui-editor/types/StageStatus.ts index 074fcfc26..b8cd73741 100644 --- a/apps/ai-game-creator-shell/src/features/ui-editor/types/StageStatus.ts +++ b/apps/ai-game-creator-shell/src/features/ui-editor/types/StageStatus.ts @@ -1,3 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type StageStatus = "Pending" | "Passed" | { "NeedReview": string } | "Blocked"; +export type StageStatus = + 'Pending' | 'Passed' | { NeedReview: string } | { Blocked: string }; diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/BindingOverview.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/BindingOverview.tsx index bc76210e6..24754facf 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/BindingOverview.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/BindingOverview.tsx @@ -3,7 +3,6 @@ import { useMemo } from 'react'; import { getBindingOverview, nodeHasBlockedComponents, - nodeHasPendingBinding, nodeNeedsComponentReview, } from '../../../features/ui-editor/bindingOverview'; import type { NodeId } from '../../../features/ui-editor/types/NodeId'; @@ -15,29 +14,24 @@ import { useUiTreeNodeCycle } from '../useUiTreeNodeCycle'; export function BindingOverview({ uiTrees, sprites, - onFocusNode, + onFocusStatusNode, }: { uiTrees: UITree[]; sprites: Record; - onFocusNode: (treeId: UIDesignImageId, nodeId: NodeId) => void; + onFocusStatusNode: (treeId: UIDesignImageId, nodeId: NodeId) => void; }) { const overview = useMemo( () => getBindingOverview(uiTrees, sprites), [sprites, uiTrees], ); - const pendingCycle = useUiTreeNodeCycle({ - uiTrees, - onFocusNode, - matches: nodeHasPendingBinding, - }); const attentionCycle = useUiTreeNodeCycle({ uiTrees, - onFocusNode, + onFocusNode: onFocusStatusNode, matches: nodeNeedsComponentReview, }); const blockedCycle = useUiTreeNodeCycle({ uiTrees, - onFocusNode, + onFocusNode: onFocusStatusNode, matches: nodeHasBlockedComponents, }); @@ -57,13 +51,7 @@ export function BindingOverview({ /> - + - + {activeStep === 'structure-recognition' ? ( + { + controller.focusNode(treeId, nodeId); + controller.highlightStatusField('layout_status'); + }} + /> + ) : activeStep === 'visual-binding' ? ( + { + controller.focusNode(treeId, nodeId); + controller.highlightStatusField('components_status'); + }} + /> + ) : ( + + )} controller.deleteNode(view.node.id)} + deleteDisabled={ + controller.editor.isLocked || + view.node.id === controller.treeForActiveImage?.root.id + } /> ); break; @@ -268,6 +275,7 @@ function NodeInspector({ keepChildrenUnchanged, onKeepChildrenUnchangedChange, onMetadataChange, + highlightedStatusField, onTransformChange, onLayoutChange, sprites, @@ -278,6 +286,8 @@ function NodeInspector({ onInsertComponent, onDeleteComponent, onMoveComponent, + onDeleteNode, + deleteDisabled, }: { node: SelectedNode; parentSize: UiEditorPageController['selectedNodeParentSize']; @@ -290,6 +300,7 @@ function NodeInspector({ keepChildrenUnchanged: boolean; onKeepChildrenUnchangedChange: (value: boolean) => void; onMetadataChange: UiEditorPageController['setNodeMetadata']; + highlightedStatusField: StageStatusField | null; onTransformChange: UiEditorPageController['setNodeTransform']; onLayoutChange: UiEditorPageController['setNodeLayout']; sprites: UiEditorPageController['sprites']; @@ -300,6 +311,8 @@ function NodeInspector({ onInsertComponent: UiEditorPageController['insertNodeComponent']; onDeleteComponent: UiEditorPageController['deleteNodeComponent']; onMoveComponent: UiEditorPageController['moveNodeComponent']; + onDeleteNode: () => void; + deleteDisabled: boolean; }) { const inspectorReadOnly = useInspectorReadOnly(); const isReadOnly = readOnly || inspectorReadOnly; @@ -388,11 +401,12 @@ function NodeInspector({ 组件:{node.components.length} -
+
{ if (!isReadOnly) onMetadataChange({ layout_status }); }} @@ -401,6 +415,7 @@ function NodeInspector({ label="组件状态" value={node.metadata.components_status} disabled={isReadOnly} + highlighted={highlightedStatusField === 'components_status'} onChange={(components_status) => { if (!isReadOnly) onMetadataChange({ components_status }); }} @@ -467,6 +482,13 @@ function NodeInspector({ onMoveComponent={onMoveComponent} /> + { + if (!deleteDisabled && !isReadOnly) onDeleteNode(); + }} + />
); } @@ -757,11 +779,13 @@ function NodeStageSelect({ label, value, disabled, + highlighted, onChange, }: { label: string; value: SelectedNode['metadata']['layout_status']; disabled: boolean; + highlighted: boolean; onChange: (value: SelectedNode['metadata']['layout_status']) => void; }) { const readOnly = useInspectorReadOnly(); @@ -771,8 +795,20 @@ function NodeStageSelect({ : 'NeedReview' in value ? 'NeedReview' : 'Blocked'; + const reason = + typeof value === 'object' + ? 'NeedReview' in value + ? value.NeedReview + : value.Blocked + : null; return ( -