diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/UiTreeRenderer.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/UiTreeRenderer.tsx index a1fb038f1..ad91c0571 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/UiTreeRenderer.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/preview/UiTreeRenderer.tsx @@ -73,7 +73,6 @@ const RESIZE_HANDLES: ReadonlyArray<{ function RenderNode({ node, isRoot, - forceVisible, parentContainer, renderMode, showFrame, @@ -93,10 +92,9 @@ function RenderNode({ }: Omit & { node: UiNode; isRoot?: boolean; - forceVisible?: boolean; parentContainer?: UiNode['layout']['container']; }) { - if (!forceVisible && hiddenNodeIds.has(node.id)) return null; + if (hiddenNodeIds.has(node.id)) return null; let geometry; try { @@ -178,15 +176,11 @@ function RenderNode({ /> ) : null} {node.children.map((child) => - exclusiveVisibleChildId && + node.children_display_mode === 'Exclusive' && child.id !== exclusiveVisibleChildId ? null : ( , hiddenNodeIds: ReadonlySet, ): NodeId | null { - return ( - children.find((child) => !hiddenNodeIds.has(child.id))?.id ?? - children[0]?.id ?? - null - ); + return children.find((child) => !hiddenNodeIds.has(child.id))?.id ?? null; } diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts index a0ecf0533..e4094c7fb 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts +++ b/apps/ai-game-creator-shell/src/view/ui-editor/useUiEditorPage.ts @@ -88,10 +88,8 @@ function isUiNodeEffectivelyVisible( targetNodeId: NodeId, hidden: ReadonlySet, ancestorsVisible = true, - forceVisible = false, ): boolean | null { - const nodeVisible = - ancestorsVisible && (forceVisible || !hidden.has(node.id)); + const nodeVisible = ancestorsVisible && !hidden.has(node.id); if (node.id === targetNodeId) return nodeVisible; const exclusiveChildId = node.children_display_mode === 'Exclusive' @@ -105,8 +103,6 @@ function isUiNodeEffectivelyVisible( nodeVisible && (node.children_display_mode !== 'Exclusive' || child.id === exclusiveChildId), - node.children_display_mode === 'Exclusive' && - child.id === exclusiveChildId, ); if (visible !== null) return visible; } @@ -446,8 +442,6 @@ export function useUiEditorSession( } } else { for (const child of parent.children) next.add(child.id); - const fallbackChildId = parent.children[0]?.id; - if (fallbackChildId) next.delete(fallbackChildId); } } else if (visible) { next.delete(nodeId); diff --git a/apps/ai-game-creator-shell/tests/exclusiveVisibility.test.ts b/apps/ai-game-creator-shell/tests/exclusiveVisibility.test.ts index f4850527d..728942e71 100644 --- a/apps/ai-game-creator-shell/tests/exclusiveVisibility.test.ts +++ b/apps/ai-game-creator-shell/tests/exclusiveVisibility.test.ts @@ -5,16 +5,20 @@ import { resolveExclusiveVisibleChildId } from '../src/view/ui-editor/components describe('resolveExclusiveVisibleChildId', () => { const children = [{ id: 'first' }, { id: 'second' }]; + it('defaults to the first child on initial load', () => { + expect(resolveExclusiveVisibleChildId(children, new Set())).toBe('first'); + }); + it('uses the first non-hidden child as the explicit selection', () => { expect(resolveExclusiveVisibleChildId(children, new Set(['first']))).toBe( 'second', ); }); - it('falls back to the first child when every child is hidden', () => { + it('keeps every child hidden after a manual hide-all action', () => { expect( resolveExclusiveVisibleChildId(children, new Set(['first', 'second'])), - ).toBe('first'); + ).toBeNull(); }); it('returns null for an empty exclusive container', () => { diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 6bc8c47ad..60470067e 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -4,7 +4,7 @@ ## 2026-08-20 UI Editor 最终预览互斥子节点 -最终预览中,选中一个 `Exclusive` 父节点时,它的子节点切换条必须在该父节点自身的预览坐标空间内、紧贴节点上方悬浮;不得固定在预览容器左上角,也不得另行按屏幕坐标换算。点击 tab 必须显式选中对应子节点,不能按通用“切换可见性”语义把当前分支隐藏。`Exclusive` 父节点没有显式可见子节点时,必须默认且仅显示第一个直接子节点;空容器不显示子节点。切换条始终按内容宽度展开并允许溢出节点边界,不设最大宽度或内部滚动区域。从 `Exclusive` 切回 `Stack` 时必须清除全部直接子节点因互斥选择产生的隐藏状态并立即显示所有子节点,后代节点自身的独立隐藏状态保持不变。切换条仅改变现有子节点可见性状态,不能触发参考图重读、视口重新适配或预览树的异步重建。 +最终预览中,选中一个 `Exclusive` 父节点时,它的子节点切换条必须在该父节点自身的预览坐标空间内、紧贴节点上方悬浮;不得固定在预览容器左上角,也不得另行按屏幕坐标换算。点击 tab 必须显式选中对应子节点,不能按通用“切换可见性”语义把当前分支隐藏。`Exclusive` 父节点首次加载且尚未发生可见性操作时,必须默认且仅显示第一个直接子节点;用户手动隐藏全部直接子节点后必须保持全部隐藏,不得再次回退显示第一个子节点;空容器不显示子节点。切换条始终按内容宽度展开并允许溢出节点边界,不设最大宽度或内部滚动区域。从 `Exclusive` 切回 `Stack` 时必须清除全部直接子节点因互斥选择产生的隐藏状态并立即显示所有子节点,后代节点自身的独立隐藏状态保持不变。切换条仅改变现有子节点可见性状态,不能触发参考图重读、视口重新适配或预览树的异步重建。 `BestFit` 文本测量的 effect 只依赖字号模式的标量值、文本测量键和实际容器,不能依赖整个 `FontSizing` 对象引用。UI State 对任意节点(包括切换 `children_display_mode`)的不可变更新会 clone 整棵树;等值 clone 不得重新创建 `ResizeObserver` 或同步重复二分测量,以避免最终预览已显示时出现明显的“重新加载”。