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 && previewUrls[activeImageId ?? ''] && logicalSize ? (
+
+
+

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