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} +