From 5feae62c572dd423e9e3bf2bb707f318ed07c970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=BE=B7=E5=AE=87?= Date: Thu, 13 Aug 2026 20:31:06 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=A9=E5=B1=95=20UI=20=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=99=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加选中节点状态管理,支持节点选择和预览交互。 - 优化 `PreviewWorkspace`,引入缩放逻辑和节点层级渲染。 - 更新 `UiTreePanel`,支持节点选中状态的 UI 表现。 - 集成节点选择状态到 `InputSidebar` 和数据模型。 - 添加 `recognizeUi` 方法,支持界面树识别响应逻辑。 --- .../ui-editor/components/InputSidebar.tsx | 7 +- .../ui-editor/components/PreviewWorkspace.tsx | 113 ++++++++++++++++-- .../view/ui-editor/components/UiTreePanel.tsx | 52 ++++++-- .../src/view/ui-editor/index.tsx | 14 +++ .../src/view/ui-editor/useUiEditorPage.ts | 46 +++++++ 5 files changed, 215 insertions(+), 17 deletions(-) diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/InputSidebar.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/InputSidebar.tsx index 370756c29..bd423401e 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/InputSidebar.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/InputSidebar.tsx @@ -17,6 +17,7 @@ export function InputSidebar({ controller }: { controller: UiEditorPageControlle spriteReferenceCounts, activeTool, treeForActiveImage, + selectedNodeId, } = controller; return ( @@ -133,7 +134,11 @@ export function InputSidebar({ controller }: { controller: UiEditorPageControlle /> {activeTool !== 'input' ? ( - + ) : null} ); diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/PreviewWorkspace.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/PreviewWorkspace.tsx index 9226d88ae..ebf9d3948 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/PreviewWorkspace.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/PreviewWorkspace.tsx @@ -1,13 +1,97 @@ +import { useEffect, useMemo, useRef, useState } from 'react'; import { Image as ImageIcon, ScanSearch } from 'lucide-react'; +import type { Node as UiNode } from '../../../features/ui-editor/types/Node'; +import { tf2css } from '../../../features/ui-editor/utils/transform/tf2css'; import type { UiEditorPageController } from '../useUiEditorPage'; +function PreviewNode({ + node, + selectedNodeId, + onSelectNode, +}: { + node: UiNode; + selectedNodeId: string | null; + onSelectNode: (id: string) => void; +}) { + let geometry; + try { + geometry = tf2css(node.transform); + } catch { + // TODO: expose malformed node diagnostics without breaking the rest of the preview. + return null; + } + + return ( +
{ + event.stopPropagation(); + onSelectNode(node.id); + }} + title={node.metadata.name || undefined} + > + {node.metadata.name ? ( + + {node.metadata.name} + + ) : null} + {/* TODO: render supported Component variants once component preview is in scope. */} + {node.children.map((child) => ( + + ))} +
+ ); +} + export function PreviewWorkspace({ controller, }: { controller: UiEditorPageController; }) { const { editor, activeImage, activeImageId, previewUrls } = controller; + const viewportRef = useRef(null); + const [scale, setScale] = useState(1); + const tree = controller.treeForActiveImage; + const logicalSize = useMemo(() => { + if (!activeImage) return null; + const ppu = activeImage.pixels_per_unit; + if (!Number.isFinite(ppu) || ppu <= 0) return null; + const width = activeImage.pixel_size[0] / ppu; + const height = activeImage.pixel_size[1] / ppu; + if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return null; + return { width, height }; + }, [activeImage]); + + useEffect(() => { + const viewport = viewportRef.current; + if (!viewport || !logicalSize) return; + const updateScale = () => { + const availableWidth = Math.max(1, viewport.clientWidth - 64); + const availableHeight = Math.max(1, viewport.clientHeight - 64); + setScale(Math.min(1, availableWidth / logicalSize.width, availableHeight / logicalSize.height)); + }; + updateScale(); + const observer = new ResizeObserver(updateScale); + observer.observe(viewport); + return () => observer.disconnect(); + }, [logicalSize]); return (
@@ -17,13 +101,28 @@ export function PreviewWorkspace({ {Object.keys(controller.sprites).length} 项素材
-
- {activeImage && previewUrls[activeImageId ?? ''] ? ( - {activeImage.metadata.name} +
+ {activeImage && previewUrls[activeImageId ?? ''] && logicalSize ? ( +
+
+ {activeImage.metadata.name} + {tree?.children.map((node) => ( + + ))} +
+
) : (
void; +}) { return (
    - {nodes.map((node, index) => ( -
  • -
    ( +
  • +
+ {node.children.length > 0 ? ( - + ) : null} ))} @@ -26,7 +48,15 @@ function TreeNodes({ nodes, depth = 0 }: { nodes: UiNode[]; depth?: number }) { ); } -export function UiTreePanel({ nodes }: { nodes: UiNode[] | null }) { +export function UiTreePanel({ + nodes, + selectedNodeId, + onSelectNode, +}: { + nodes: UiNode[] | null; + selectedNodeId: NodeId | null; + onSelectNode: (id: NodeId) => void; +}) { return (
@@ -35,7 +65,11 @@ export function UiTreePanel({ nodes }: { nodes: UiNode[] | null }) {
{nodes ? ( - + ) : (

当前界面尚无组件树。 diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx index 5601cd7e5..54679c4d8 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/index.tsx @@ -30,6 +30,20 @@ export default function UiEditorPage({ {controller.suggestionStatus}

) : null} + {controller.recognitionStatus ? ( +

+ {controller.recognitionStatus} +

+ ) : null} +