diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/EditorDialogs.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/EditorDialogs.tsx index 1bad535ed..60ed0142b 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/EditorDialogs.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/EditorDialogs.tsx @@ -84,35 +84,6 @@ export function EditorDialogs({ - - -

确认移除页面组件树?

-

- 这张界面图当前是 Page,已有 UI - Tree。改为其它界面角色会自动删除页面根节点及其全部子节点,操作不可恢复。 -

-
- - -
-
); } diff --git a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx index a520f5397..22ba567eb 100644 --- a/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx +++ b/apps/ai-game-creator-shell/src/view/ui-editor/components/Inspector/InspectorSidebar.tsx @@ -11,6 +11,7 @@ import type { UIDesignImageRole } from '../../../../features/ui-editor/types/UID import { UI_DESIGN_IMAGE_ROLES } from '../../model'; import type { UiEditorPageController } from '../../useUiEditorPage'; import { PrerequisiteIssues } from '../PrerequisiteIssues'; +import { ComponentPanel } from './Components/ComponentPanel'; import { SpriteBorderEditor } from './SpriteBorder/SpriteBorderEditor'; import TransformEditor from './Transform/TransformEditor'; @@ -59,6 +60,68 @@ export function InspectorSidebar({ controller: UiEditorPageController; }) { const view = getInspectorView(controller); + let inspectorContent: ReactNode; + switch (view.kind) { + case 'node': + inspectorContent = ( + + ); + break; + case 'sprite': + inspectorContent = ( + controller.requestSpriteRemoval(view.spriteId)} + deleteDisabled={controller.editor.isLocked} + /> + ); + break; + case 'image': + inspectorContent = ( + controller.requestDesignImageRemoval(view.imageId)} + deleteDisabled={controller.editor.isLocked} + /> + ); + break; + case 'empty': + inspectorContent = ( +

+ 选择一张界面图后可编辑其属性。 +

+ ); + break; + } return ( ); } @@ -137,75 +185,61 @@ function getInspectorView(controller: UiEditorPageController): InspectorView { return { kind: 'empty' }; } -function InspectorContent({ - controller, - view, -}: { - controller: UiEditorPageController; - view: InspectorView; -}) { - switch (view.kind) { - case 'node': - return ( - - ); - case 'sprite': - return ( - - ); - case 'image': - return ( - - ); - case 'empty': - return ( -

- 选择一张界面图后可编辑其属性。 -

- ); - } -} - function NodeInspector({ node, parentSize, readOnly, + keepChildrenUnchanged, + onKeepChildrenUnchangedChange, onMetadataChange, onTransformChange, + sprites, + fonts, + onSetComponents, + onInsertComponent, + onDeleteComponent, + onMoveComponent, }: { node: SelectedNode; parentSize: UiEditorPageController['selectedNodeParentSize']; readOnly: boolean; + keepChildrenUnchanged: boolean; + onKeepChildrenUnchangedChange: (value: boolean) => void; onMetadataChange: UiEditorPageController['setNodeMetadata']; onTransformChange: UiEditorPageController['setNodeTransform']; + sprites: UiEditorPageController['sprites']; + fonts: UiEditorPageController['editor']['state']['font_assets']; + onSetComponents: UiEditorPageController['setNodeComponents']; + onInsertComponent: UiEditorPageController['insertNodeComponent']; + onDeleteComponent: UiEditorPageController['deleteNodeComponent']; + onMoveComponent: UiEditorPageController['moveNodeComponent']; }) { return (
+
+ + +
-

- 组件预览与编辑待支持。 -

+
); @@ -248,6 +289,8 @@ function SpriteInspector({ onNameChange, onAssetTypeChange, onBorderChange, + onDelete, + deleteDisabled, }: { sprite: SelectedSprite; spriteId: SelectedSpriteId; @@ -256,6 +299,8 @@ function SpriteInspector({ onNameChange: UiEditorPageController['setSpriteName']; onAssetTypeChange: UiEditorPageController['setSpriteAssetType']; onBorderChange: UiEditorPageController['setSpriteBorder']; + onDelete: () => void; + deleteDisabled: boolean; }) { return (
@@ -293,6 +338,11 @@ function SpriteInspector({ onChange={onBorderChange} /> +
); } @@ -305,6 +355,8 @@ function ImageInspector({ onDescriptionChange, onRoleChange, onSlaveToChange, + onDelete, + deleteDisabled, }: { image: ActiveImage; imageId: ActiveImageId; @@ -313,6 +365,8 @@ function ImageInspector({ onDescriptionChange: UiEditorPageController['setImageDescription']; onRoleChange: UiEditorPageController['setImageRole']; onSlaveToChange: UiEditorPageController['setImageSlaveTo']; + onDelete: () => void; + deleteDisabled: boolean; }) { return (
@@ -366,6 +420,11 @@ function ImageInspector({ ) : 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/preview/PreviewWorkspace.tsx similarity index 66% rename from apps/ai-game-creator-shell/src/view/ui-editor/components/PreviewWorkspace.tsx rename to apps/ai-game-creator-shell/src/view/ui-editor/components/preview/PreviewWorkspace.tsx index 59ed041b4..5425f495e 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/preview/PreviewWorkspace.tsx @@ -1,3 +1,18 @@ +import { + type CanvasViewport, + createPanDragState, + type DragState, + fitViewportToBounds, + moveViewportFromPan, + resolveViewportFromWheel, + scaleViewportFromScreenPoint, +} from '@genarrative/image-canvas-core'; +import { + CanvasViewport as SharedCanvasViewport, + CanvasWorld, + ZoomControls, +} from '@genarrative/image-canvas-react'; +import { Image as ImageIcon, Minus, Plus, ScanSearch } from 'lucide-react'; import { type PointerEvent as ReactPointerEvent, useCallback, @@ -6,108 +21,10 @@ import { useRef, useState, } from 'react'; -import { Image as ImageIcon, Minus, Plus, ScanSearch } from 'lucide-react'; -import { - createPanDragState, - fitViewportToBounds, - moveViewportFromPan, - resolveViewportFromWheel, - scaleViewportFromScreenPoint, - type CanvasViewport, - type DragState, -} from '@genarrative/image-canvas-core'; -import { - CanvasViewport as SharedCanvasViewport, - CanvasWorld, - ZoomControls, -} from '@genarrative/image-canvas-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'; - -type NodeDragState = { - nodeId: string; - pointerId: number; - startClientX: number; - startClientY: number; - startTransform: UiNode['transform']; -}; - -function PreviewNode({ - node, - isRoot = false, - selectedNodeId, - onSelectNode, - onNodePointerDown, - onNodePointerMove, - onNodePointerUp, -}: { - node: UiNode; - isRoot?: boolean; - selectedNodeId: string | null; - onSelectNode: (id: string) => void; - onNodePointerDown: ( - event: ReactPointerEvent, - node: UiNode, - ) => void; - onNodePointerMove: (event: ReactPointerEvent) => void; - onNodePointerUp: (event: ReactPointerEvent) => 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); - }} - onPointerDown={(event) => onNodePointerDown(event, node)} - onPointerMove={onNodePointerMove} - onPointerUp={onNodePointerUp} - onPointerCancel={onNodePointerUp} - 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) => ( - - ))} -
- ); -} +import type { UiEditorPageController } from '../../useUiEditorPage'; +import { type UiEditorRenderMode, UiTreeRenderer } from './UiTreeRenderer'; +import { useNodeTransformInteraction } from './useNodeTransformInteraction'; export function PreviewWorkspace({ controller, @@ -118,13 +35,14 @@ export function PreviewWorkspace({ const viewportElementRef = useRef(null); const viewportRef = useRef({ x: 0, y: 0, scale: 0.5 }); const panRef = useRef | null>(null); - const nodeDragRef = useRef(null); const [viewport, setViewportState] = useState( viewportRef.current, ); const [canvasSize, setCanvasSize] = useState({ width: 900, height: 640 }); const [spaceHeld, setSpaceHeld] = useState(false); - const tree = controller.treeForActiveImage; + const [renderMode, setRenderMode] = + useState('editor-overlay'); + const tree = controller.treeForActiveImage ?? null; const logicalSize = useMemo(() => { if (!activeImage) return null; const ppu = activeImage.pixels_per_unit; @@ -146,6 +64,23 @@ export function PreviewWorkspace({ activeImage?.pixels_per_unit, ]); + const nodeInteractions = useNodeTransformInteraction({ + activeImageId, + controller, + logicalSize, + spaceHeld, + tree, + viewportRef, + }); + const { + onNodePointerDown, + onNodePointerMove, + onNodePointerUp, + onNodeResizePointerDown, + onNodeResizePointerMove, + onNodeResizePointerUp, + } = nodeInteractions; + const setViewport = useCallback((next: CanvasViewport) => { viewportRef.current = next; setViewportState(next); @@ -241,54 +176,6 @@ export function PreviewWorkspace({ }; }, [fitToCanvas, logicalSize, setViewport]); - const handleNodePointerDown = useCallback( - (event: ReactPointerEvent, node: UiNode) => { - if (event.button !== 0 || spaceHeld || node.id === tree?.root.id) return; - event.stopPropagation(); - event.currentTarget.setPointerCapture(event.pointerId); - controller.selectNode(node.id); - nodeDragRef.current = { - nodeId: node.id, - pointerId: event.pointerId, - startClientX: event.clientX, - startClientY: event.clientY, - startTransform: structuredClone(node.transform), - }; - }, - [controller, spaceHeld, tree?.root.id], - ); - - const handleNodePointerMove = useCallback( - (event: ReactPointerEvent) => { - event.stopPropagation(); - const drag = nodeDragRef.current; - if (!drag || drag.pointerId !== event.pointerId || !activeImageId) return; - const logicalDeltaX = - (event.clientX - drag.startClientX) / viewportRef.current.scale; - const logicalDeltaY = - (event.clientY - drag.startClientY) / viewportRef.current.scale; - const nextTransform = structuredClone(drag.startTransform); - nextTransform.offset_min[0] += logicalDeltaX; - nextTransform.offset_min[1] += logicalDeltaY; - nextTransform.offset_max[0] += logicalDeltaX; - nextTransform.offset_max[1] += logicalDeltaY; - controller.updateNodeTransform(activeImageId, drag.nodeId, nextTransform); - }, - [activeImageId, controller], - ); - - const handleNodePointerUp = useCallback( - (event: ReactPointerEvent) => { - event.stopPropagation(); - if (nodeDragRef.current?.pointerId !== event.pointerId) return; - nodeDragRef.current = null; - if (event.currentTarget.hasPointerCapture(event.pointerId)) { - event.currentTarget.releasePointerCapture(event.pointerId); - } - }, - [], - ); - const handlePointerDown = (event: ReactPointerEvent) => { if (event.button === 1 || (event.button === 0 && spaceHeld)) { event.preventDefault(); @@ -346,12 +233,34 @@ export function PreviewWorkspace({

{controller.activeToolLabel}

- - {Object.keys(controller.images).length} 张界面图 ·{' '} - {Object.keys(controller.sprites).length} 项素材 - +
+
+ + +
+ + {Object.keys(controller.images).length} 张界面图 ·{' '} + {Object.keys(controller.sprites).length} 项素材 + +
- {activeImage && previewUrls[activeImageId ?? ''] && logicalSize ? ( + {activeImage && + logicalSize && + (renderMode === 'final-preview' || previewUrls[activeImageId ?? '']) ? (
- {activeImage.metadata.name} - {tree?.root ? ( - - ) : null} + ) : ( +
+ )} +