UI编辑器重构多树预览与独立开关

预览画布同时渲染全部界面树并支持树级拖动

拆分显示框线、原图和组件开关

预览节点选择与左侧当前图片解耦
This commit is contained in:
2026-09-14 18:05:50 +08:00
parent 46f8592375
commit ec94a4fe00
5 changed files with 264 additions and 157 deletions
@@ -90,10 +90,8 @@ export function InputSidebar({ input }: { input: UiEditorInputProjection }) {
// programmatic notification as a fresh user selection would clear
// the highlight before it can be painted. Only mutate selection
// state when the target actually differs from the current one.
const sameImage = input.activeImageId === treeId;
const sameNode = input.selectedNodeId === nodeId;
if (!sameImage) input.selectDesignImage(treeId);
if (!sameImage || !sameNode) input.selectNode(nodeId);
if (!sameNode) input.selectNode(nodeId);
}}
onToggleNodeVisibility={(nodeId) =>
input.toggleNodePreviewVisibility(nodeId)
@@ -38,7 +38,7 @@ import {
isPreviewZoomInteractiveTarget,
previewZoomUsesMetaModifier,
} from './previewZoomKeyboard';
import { type UiEditorRenderMode, UiTreeRenderer } from './UiTreeRenderer';
import { UiTreeRenderer } from './UiTreeRenderer';
import { useNodeTransformInteraction } from './useNodeTransformInteraction';
/** 画布初始适配比例;调小可为浮动面板预留更多可视空间。 */
@@ -57,29 +57,34 @@ function fitEditorViewport(
};
}
export type { UiEditorRenderMode } from './UiTreeRenderer';
export function PreviewWorkspace({
canvas,
renderMode: controlledRenderMode,
onRenderModeChange: _onRenderModeChange,
showFrame: controlledShowFrame,
onShowFrameChange: _onShowFrameChange,
showFrame,
showOriginImage,
showComponent,
}: {
canvas: UiEditorCanvasProjection;
renderMode?: UiEditorRenderMode;
onRenderModeChange?: (mode: UiEditorRenderMode) => void;
showFrame?: boolean;
onShowFrameChange?: (show: boolean) => void;
showFrame: boolean;
showOriginImage: boolean;
showComponent: boolean;
}) {
const { activeImage, activeImageId, previewUrls } = canvas;
const activePreviewUrl = activeImageId
? previewUrls[activeImageId]
: undefined;
const { activeImage, activeImageId, previewUrls, uiTrees, images } = canvas;
const tree = canvas.tree ?? null;
const viewportElementRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<CanvasViewport>({ x: 0, y: 0, scale: 0.5 });
const handledFocusRequestIdRef = useRef<number | null>(null);
const didInitialFitRef = useRef(false);
const panRef = useRef<Extract<DragState, { kind: 'pan' }> | null>(null);
const treeDragRef = useRef<{
treeId: string;
pointerId: number;
startX: number;
startY: number;
origin: [number, number];
} | null>(null);
const [previewTreeOffsets, setPreviewTreeOffsets] = useState<
ReadonlyMap<string, [number, number]>
>(new Map());
const previewFocusedRef = useRef(false);
const previewHoveredRef = useRef(false);
const [viewport, setViewportState] = useState<CanvasViewport>(
@@ -88,8 +93,6 @@ export function PreviewWorkspace({
const [canvasSize, setCanvasSize] = useState({ width: 900, height: 640 });
const canvasSizeRef = useRef(canvasSize);
const [spaceHeld, setSpaceHeld] = useState(false);
const renderMode = controlledRenderMode ?? 'editor-overlay';
const showFrame = controlledShowFrame ?? false;
const [previewTransforms, setPreviewTransforms] = useState<
ReadonlyMap<NodeId, Node['layout']['transform']>
>(new Map());
@@ -99,7 +102,6 @@ export function PreviewWorkspace({
y: number;
isPageRoot: boolean;
} | null>(null);
const tree = canvas.tree ?? null;
const updatePreviewTransform = useCallback(
(nodeId: NodeId, transform: Node['layout']['transform'] | null) => {
setPreviewTransforms((current) => {
@@ -141,6 +143,38 @@ export function PreviewWorkspace({
}
return { width, height };
}, [activeImagePixelHeight, activeImagePixelsPerUnit, activeImagePixelWidth]);
const treeLayouts = useMemo(
() =>
uiTrees.map((item) => {
const image = images[item.src_ui_design];
const ppu = image?.pixels_per_unit ?? 0;
const width = image ? image.pixel_size[0] / ppu : 0;
const height = image ? image.pixel_size[1] / ppu : 0;
return {
tree: item,
image,
width,
height,
validOffset: Boolean(item.root.offset?.min),
};
}),
[images, uiTrees],
);
const allBounds = useMemo(() => {
const valid = treeLayouts.filter(
(item) => item.validOffset && item.width > 0 && item.height > 0,
);
if (valid.length === 0) return null;
const minX = Math.min(...valid.map(({ tree }) => tree.root.offset.min[0]));
const minY = Math.min(...valid.map(({ tree }) => tree.root.offset.min[1]));
const maxX = Math.max(
...valid.map(({ tree, width }) => tree.root.offset.min[0] + width),
);
const maxY = Math.max(
...valid.map(({ tree, height }) => tree.root.offset.min[1] + height),
);
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
}, [treeLayouts]);
const nodeInteractions = useNodeTransformInteraction({
activeImageId,
@@ -172,40 +206,27 @@ export function PreviewWorkspace({
}, []);
const fitToCanvas = useCallback(() => {
if (!logicalSize) return;
if (!allBounds) return;
const element = viewportElementRef.current;
setViewport(
fitEditorViewport(
{ x: 0, y: 0, width: logicalSize.width, height: logicalSize.height },
{
width: element?.clientWidth || canvasSizeRef.current.width,
height: element?.clientHeight || canvasSizeRef.current.height,
},
),
fitEditorViewport(allBounds, {
width: element?.clientWidth || canvasSizeRef.current.width,
height: element?.clientHeight || canvasSizeRef.current.height,
}),
);
}, [logicalSize, setViewport]);
}, [allBounds, setViewport]);
useEffect(() => {
if (!activeImageId || !logicalSize) return;
if (!allBounds) return;
if (didInitialFitRef.current) return;
didInitialFitRef.current = true;
const element = viewportElementRef.current;
const canvasSize = {
width: element?.clientWidth || canvasSizeRef.current.width,
height: element?.clientHeight || canvasSizeRef.current.height,
};
setViewport(
fitEditorViewport(
{ x: 0, y: 0, width: logicalSize.width, height: logicalSize.height },
canvasSize,
),
);
}, [
activeImageId,
activePreviewUrl,
canvasSize.height,
canvasSize.width,
logicalSize,
setViewport,
]);
setViewport(fitEditorViewport(allBounds, canvasSize));
}, [allBounds, canvasSize.height, canvasSize.width, setViewport]);
const scaleViewportFromCenter = useCallback(
(nextScale: number) => {
@@ -389,6 +410,53 @@ export function PreviewWorkspace({
}
};
const handleTreePointerDown = (
event: ReactPointerEvent<HTMLDivElement>,
treeId: string,
rootId: string,
origin: [number, number],
) => {
if (event.button !== 0) return;
const hit = (event.target as Element).closest('[data-node-id]');
if (!hit || hit.getAttribute('data-node-id') !== rootId) return;
event.stopPropagation();
event.currentTarget.setPointerCapture(event.pointerId);
treeDragRef.current = {
treeId,
pointerId: event.pointerId,
startX: event.clientX,
startY: event.clientY,
origin,
};
};
const handleTreePointerMove = (event: ReactPointerEvent<HTMLDivElement>) => {
const drag = treeDragRef.current;
if (!drag || drag.pointerId !== event.pointerId) return;
const next: [number, number] = [
drag.origin[0] +
(event.clientX - drag.startX) / viewportRef.current.scale,
drag.origin[1] +
(event.clientY - drag.startY) / viewportRef.current.scale,
];
setPreviewTreeOffsets((current) => new Map(current).set(drag.treeId, next));
};
const handleTreePointerUp = (event: ReactPointerEvent<HTMLDivElement>) => {
if (treeDragRef.current?.pointerId !== event.pointerId) return;
const drag = treeDragRef.current;
if (drag) {
const next = previewTreeOffsets.get(drag.treeId);
if (next) canvas.setTreeOffset(drag.treeId, next);
setPreviewTreeOffsets((current) => {
const result = new Map(current);
result.delete(drag.treeId);
return result;
});
}
treeDragRef.current = null;
if (event.currentTarget.hasPointerCapture(event.pointerId))
event.currentTarget.releasePointerCapture(event.pointerId);
};
const handleWheel = (event: React.WheelEvent<HTMLDivElement>) => {
event.preventDefault();
const rect = event.currentTarget.getBoundingClientRect();
@@ -421,15 +489,9 @@ export function PreviewWorkspace({
});
};
useEffect(() => {
if (renderMode !== 'editor-overlay') setContextMenu(null);
}, [renderMode]);
return (
<section className="flex h-full min-h-0 min-w-0 flex-col overflow-hidden bg-[#f8f4ef]">
{activeImage &&
logicalSize &&
(renderMode === 'final-preview' || previewUrls[activeImageId ?? '']) ? (
{treeLayouts.length > 0 && allBounds ? (
<div className="relative min-h-0 flex-1">
<SharedCanvasViewport
ref={viewportElementRef}
@@ -471,50 +533,66 @@ export function PreviewWorkspace({
}}
>
<CanvasWorld viewport={viewport} worldSize={CANVAS_WORLD_SIZE}>
<div
className="relative"
style={{ width: logicalSize.width, height: logicalSize.height }}
>
{renderMode === 'editor-overlay' ? (
<img
src={previewUrls[activeImageId ?? '']}
alt={activeImage.metadata.name}
className="pointer-events-none absolute inset-0 size-full object-fill shadow-[0_18px_50px_rgb(67_48_37_/_14%)]"
draggable={false}
/>
) : (
<div
className="absolute inset-0 bg-[#f7f4ef] shadow-[0_18px_50px_rgb(67_48_37_/_14%)]"
aria-label="最终组件预览画布"
/>
)}
<UiTreeRenderer
tree={tree}
renderMode={renderMode}
showFrame={showFrame}
hiddenNodeIds={canvas.hiddenNodeIds}
previewTransforms={previewTransforms}
selectedNodeId={canvas.selectedNodeId}
resources={{
previewUrls,
sprites: canvas.sprites,
fontFaces: canvas.fontFaces,
}}
onSelectNode={canvas.selectNode}
consumeNodeClick={consumeNodeClick}
onNodeContextMenu={handleNodeContextMenu}
onNodePointerDown={onNodePointerDown}
onNodePointerMove={onNodePointerMove}
onNodePointerUp={onNodePointerUp}
onNodePointerCancel={onNodePointerCancel}
onNodeResizePointerDown={onNodeResizePointerDown}
onNodeResizePointerMove={onNodeResizePointerMove}
onNodeResizePointerUp={onNodeResizePointerUp}
onNodeResizePointerCancel={onNodeResizePointerCancel}
onSelectExclusiveChild={canvas.selectExclusiveChild}
viewportScale={viewport.scale}
/>
</div>
{treeLayouts
.filter((item) => item.validOffset)
.map(({ tree: item, image, width, height }) => {
const offset =
previewTreeOffsets.get(item.src_ui_design) ??
item.root.offset.min;
return (
<div
key={item.src_ui_design}
className="absolute"
style={{ left: offset[0], top: offset[1], width, height }}
onPointerDown={(event) =>
handleTreePointerDown(
event,
item.src_ui_design,
item.root.id,
offset,
)
}
onPointerMove={handleTreePointerMove}
onPointerUp={handleTreePointerUp}
onPointerCancel={handleTreePointerUp}
>
{showOriginImage && previewUrls[item.src_ui_design] ? (
<img
src={previewUrls[item.src_ui_design]}
alt={image?.metadata.name ?? ''}
className="pointer-events-none absolute inset-0 size-full object-fill shadow-[0_18px_50px_rgb(67_48_37_/_14%)]"
draggable={false}
/>
) : null}
<UiTreeRenderer
tree={item}
showFrame={showFrame}
showComponent={showComponent}
hiddenNodeIds={canvas.hiddenNodeIds}
previewTransforms={previewTransforms}
selectedNodeId={canvas.selectedNodeId}
resources={{
previewUrls,
sprites: canvas.sprites,
fontFaces: canvas.fontFaces,
}}
onSelectNode={canvas.selectNode}
consumeNodeClick={consumeNodeClick}
onNodeContextMenu={handleNodeContextMenu}
onNodePointerDown={onNodePointerDown}
onNodePointerMove={onNodePointerMove}
onNodePointerUp={onNodePointerUp}
onNodePointerCancel={onNodePointerCancel}
onNodeResizePointerDown={onNodeResizePointerDown}
onNodeResizePointerMove={onNodeResizePointerMove}
onNodeResizePointerUp={onNodeResizePointerUp}
onNodeResizePointerCancel={onNodeResizePointerCancel}
onSelectExclusiveChild={canvas.selectExclusiveChild}
viewportScale={viewport.scale}
/>
</div>
);
})}
</CanvasWorld>
</SharedCanvasViewport>
{contextMenu ? (
@@ -29,8 +29,8 @@ type NodePointerDown = (
type UiTreeRendererProps = {
tree: UITree | null;
renderMode: UiEditorRenderMode;
showFrame: boolean;
showComponent: boolean;
hiddenNodeIds: ReadonlySet<NodeId>;
previewTransforms?: ReadonlyMap<NodeId, UiNode['layout']['transform']>;
selectedNodeId: NodeId | null;
@@ -83,8 +83,8 @@ function RenderNode({
node,
isRoot,
parentContainer,
renderMode,
showFrame,
showComponent,
hiddenNodeIds,
previewTransforms,
selectedNodeId,
@@ -124,8 +124,8 @@ function RenderNode({
return null;
}
const isEditorOverlay = renderMode === 'editor-overlay';
const isFrameVisible = isEditorOverlay || showFrame;
const isSelected = selectedNodeId === node.id;
const isFrameVisible = showFrame || isSelected;
const receivesPointerGesture = parentContainer === undefined;
const hasDirectPointerGesture = receivesPointerGesture && !isRoot;
const exclusiveVisibleChildId =
@@ -170,7 +170,6 @@ function RenderNode({
}
}
onContextMenu={(event) => {
if (!isEditorOverlay) return;
event.preventDefault();
event.stopPropagation();
onSelectNode(node.id);
@@ -191,10 +190,10 @@ function RenderNode({
{node.metadata.name}
</span>
) : null}
{node.component ? (
{showComponent && node.component ? (
<ComponentView component={node.component} resources={resources} />
) : null}
{renderMode === 'final-preview' && selectedNodeId === node.id ? (
{isSelected ? (
<ExclusiveChildrenTabs
parent={node}
isChildVisible={(nodeId) => nodeId === exclusiveVisibleChildId}
@@ -212,8 +211,8 @@ function RenderNode({
? node.layout.container
: undefined
}
renderMode={renderMode}
showFrame={showFrame}
showComponent={showComponent}
hiddenNodeIds={hiddenNodeIds}
previewTransforms={activePreviewTransforms}
selectedNodeId={selectedNodeId}
@@ -12,10 +12,7 @@ import { EditorDialogs } from './components/EditorDialogs';
import { ImportOverview } from './components/ImportOverview';
import { InputSidebar } from './components/InputSidebar';
import { InspectorSidebar } from './components/Inspector/InspectorSidebar';
import {
PreviewWorkspace,
type UiEditorRenderMode,
} from './components/preview/PreviewWorkspace';
import { PreviewWorkspace } from './components/preview/PreviewWorkspace';
import { RecognitionOverview } from './components/RecognitionOverview';
import { SeparationOverview } from './components/SeparationOverview';
import { ToolNavigation } from './components/ToolNavigation';
@@ -75,9 +72,9 @@ export default function UiEditorPage({
const [generateAfterWarning, setGenerateAfterWarning] = useState(false);
const [generateSuccess, setGenerateSuccess] = useState<string | null>(null);
const [returnConfirmOpen, setReturnConfirmOpen] = useState(false);
const [renderMode, setRenderMode] =
useState<UiEditorRenderMode>('editor-overlay');
const [showFrame, setShowFrame] = useState(false);
const [showFrame, setShowFrame] = useState(true);
const [showOriginImage, setShowOriginImage] = useState(true);
const [showComponent, setShowComponent] = useState(false);
const overview =
session.input.activeStep === 'structure-recognition' ? (
<RecognitionOverview
@@ -200,26 +197,8 @@ export default function UiEditorPage({
<header className="absolute left-1/2 top-4 z-30 flex w-[min(1000px,calc(100%_-_2rem))] -translate-x-1/2 flex-wrap items-center justify-between gap-2 rounded-xl border border-(--platform-subpanel-border) [background:var(--platform-subpanel-fill)] px-4 py-2 shadow-xl backdrop-blur">
<strong className="text-sm">{resourceLabel ?? 'UI 设计'}</strong>
<div className="game-workbench-editor-actions">
<div className="flex items-center rounded-lg border border-(--platform-subpanel-border) bg-white/70 p-0.5 text-xs">
<button
type="button"
className={`rounded-md px-2 py-1 ${renderMode === 'editor-overlay' ? 'bg-blue-600 text-white' : 'text-(--platform-text-soft)'}`}
aria-pressed={renderMode === 'editor-overlay'}
onClick={() => setRenderMode('editor-overlay')}
>
</button>
<button
type="button"
className={`rounded-md px-2 py-1 ${renderMode === 'final-preview' ? 'bg-orange-500 text-white' : 'text-(--platform-text-soft)'}`}
aria-pressed={renderMode === 'final-preview'}
onClick={() => setRenderMode('final-preview')}
>
</button>
</div>
{renderMode === 'final-preview' ? (
<label className="inline-flex cursor-pointer select-none items-center gap-1.5 rounded-lg px-2 py-1 text-xs text-(--platform-text-soft)">
<div className="flex items-center gap-2 text-xs text-(--platform-text-soft)">
<label className="inline-flex cursor-pointer select-none items-center gap-1.5 rounded-lg px-2 py-1">
<input
type="checkbox"
checked={showFrame}
@@ -228,7 +207,25 @@ export default function UiEditorPage({
/>
<span>线</span>
</label>
) : null}
<label className="inline-flex cursor-pointer select-none items-center gap-1.5 rounded-lg px-2 py-1">
<input
type="checkbox"
checked={showOriginImage}
onChange={(event) => setShowOriginImage(event.target.checked)}
className="size-3 accent-orange-500"
/>
<span></span>
</label>
<label className="inline-flex cursor-pointer select-none items-center gap-1.5 rounded-lg px-2 py-1">
<input
type="checkbox"
checked={showComponent}
onChange={(event) => setShowComponent(event.target.checked)}
className="size-3 accent-orange-500"
/>
<span></span>
</label>
</div>
<button
type="button"
className="inline-flex items-center gap-1 rounded-lg border border-(--platform-subpanel-border) px-2.5 py-1.5 text-sm disabled:cursor-not-allowed disabled:opacity-40"
@@ -319,10 +316,9 @@ export default function UiEditorPage({
<div className="absolute inset-0 min-h-0 min-w-0 overflow-hidden">
<PreviewWorkspace
canvas={session.canvas}
renderMode={renderMode}
onRenderModeChange={setRenderMode}
showFrame={showFrame}
onShowFrameChange={setShowFrame}
showOriginImage={showOriginImage}
showComponent={showComponent}
/>
<div className="pointer-events-none absolute inset-0 z-10">
<div
@@ -43,6 +43,7 @@ import { applyUiDesignSuggestions } from '../../features/ui-editor/uiDesignSugge
import { useUiEditorFontFaces } from '../../features/ui-editor/useUiEditorFontFaces';
import { addSpriteAssetsToState } from '../../features/ui-editor/useUiEditorState';
import {
createTree,
EMPTY_UI_EDITOR_STATE,
type NodeLayoutPatch,
type NodeMetadataPatch,
@@ -196,6 +197,7 @@ export function useUiEditorSession(
) {
const editor = useUiEditorState(EMPTY_UI_EDITOR_STATE);
const editorDeleteNode = editor.deleteNode;
const setTreeOffset = editor.setTreeOffset;
const editorUiTrees = editor.state.ui_trees;
const replaceEditorState = editor.replaceState;
const [isLoading, setIsLoading] = useState(Boolean(resourceId));
@@ -463,6 +465,16 @@ export function useUiEditorSession(
? editor.state.ui_trees.find((tree) => tree.src_ui_design === activeImageId)
: null;
const treeForSelectedNode = useMemo(() => {
if (!selectedNodeId) return null;
return (
editor.state.ui_trees.find(
(candidate) =>
findUiNodeLocation(candidate.root, selectedNodeId) !== null,
) ?? null
);
}, [editor.state.ui_trees, selectedNodeId]);
useEffect(() => {
const validNodeIds = new Set<NodeId>();
for (const tree of editor.state.ui_trees) {
@@ -564,16 +576,18 @@ export function useUiEditorSession(
[setNodePreviewVisible],
);
const selectedNodeContext = useMemo(() => {
if (!activeImage || !treeForActiveImage || !selectedNodeId) return null;
const ppu = activeImage.pixels_per_unit;
const width = activeImage.pixel_size[0] / ppu;
const height = activeImage.pixel_size[1] / ppu;
if (!selectedNodeId || !treeForSelectedNode) return null;
const image = images[treeForSelectedNode.src_ui_design];
if (!image) return null;
const ppu = image.pixels_per_unit;
const width = image.pixel_size[0] / ppu;
const height = image.pixel_size[1] / ppu;
if (!Number.isFinite(width) || !Number.isFinite(height)) return null;
return findNodeContext(treeForActiveImage.root, selectedNodeId, {
return findNodeContext(treeForSelectedNode.root, selectedNodeId, {
width,
height,
});
}, [activeImage, selectedNodeId, treeForActiveImage]);
}, [images, selectedNodeId, treeForSelectedNode]);
async function importAssets(imported: ImportedAsset[]) {
if (!importKind || !projectPath) return;
@@ -779,8 +793,12 @@ export function useUiEditorSession(
}
function setNodeTransform(transform: UiNode['layout']['transform']) {
if (!activeImageId || !selectedNodeId) return;
return updateNodeTransform(activeImageId, selectedNodeId, transform);
if (!treeForSelectedNode?.src_ui_design || !selectedNodeId) return;
return updateNodeTransform(
treeForSelectedNode.src_ui_design,
selectedNodeId,
transform,
);
}
function updateNodeTransform(
@@ -799,8 +817,12 @@ export function useUiEditorSession(
}
function setNodeMetadata(patch: NodeMetadataPatch) {
if (!activeImageId || !selectedNodeId) return;
const result = editor.setNodeMetadata(activeImageId, selectedNodeId, patch);
if (!treeForSelectedNode?.src_ui_design || !selectedNodeId) return;
const result = editor.setNodeMetadata(
treeForSelectedNode.src_ui_design,
selectedNodeId,
patch,
);
if (
result.ok &&
(patch.layout_status !== undefined ||
@@ -813,19 +835,23 @@ export function useUiEditorSession(
}
function setNodeLayout(patch: NodeLayoutPatch) {
if (!activeImageId || !selectedNodeId) return;
const result = editor.setNodeLayout(activeImageId, selectedNodeId, patch);
if (!treeForSelectedNode?.src_ui_design || !selectedNodeId) return;
const result = editor.setNodeLayout(
treeForSelectedNode.src_ui_design,
selectedNodeId,
patch,
);
if (!result.ok) setStatus('节点布局更新失败。');
return result;
}
function setNodeChildrenDisplayMode(mode: ChildrenDisplayMode) {
if (!activeImageId || !selectedNodeId) return;
const selectedNode = treeForActiveImage
? findUiNodeLocation(treeForActiveImage.root, selectedNodeId)?.node
if (!treeForSelectedNode?.src_ui_design || !selectedNodeId) return;
const selectedNode = treeForSelectedNode
? findUiNodeLocation(treeForSelectedNode.root, selectedNodeId)?.node
: null;
const result = editor.setNodeChildrenDisplayMode(
activeImageId,
treeForSelectedNode.src_ui_design,
selectedNodeId,
mode,
);
@@ -844,9 +870,9 @@ export function useUiEditorSession(
}
function setNodeComponent(component: Component | null) {
if (!activeImageId || !selectedNodeId) return;
if (!treeForSelectedNode?.src_ui_design || !selectedNodeId) return;
const result = editor.setNodeComponent(
activeImageId,
treeForSelectedNode.src_ui_design,
selectedNodeId,
component,
);
@@ -1002,6 +1028,9 @@ export function useUiEditorSession(
state: snapshot,
});
const nextState = applyRecognitionResult(snapshot, result);
nextState.ui_trees = nextState.ui_trees.map((tree) =>
createTree(nextState, tree.src_ui_design, tree.root),
);
editor.replaceState(nextState);
setHasRecognized(true);
setSelectedNodeId(null);
@@ -1036,7 +1065,11 @@ export function useUiEditorSession(
try {
await editor.runWithStateLocked(async (snapshot) => {
const result = await invoke<MergeDTO>('merge_ui', { state: snapshot });
editor.replaceState(applyMergeResult(snapshot, result));
const nextState = applyMergeResult(snapshot, result);
nextState.ui_trees = nextState.ui_trees.map((tree) =>
createTree(nextState, tree.src_ui_design, tree.root),
);
editor.replaceState(nextState);
setSelectedNodeId(null);
setMergeStatus('已生成合并后的单棵界面树。');
});
@@ -1518,11 +1551,14 @@ export function useUiEditorSession(
isLocked: editor.isLocked,
activeImage,
activeImageId,
uiTrees: editor.state.ui_trees,
previewUrls,
images,
sprites,
fontFaces,
tree: treeForActiveImage ?? null,
selectedTreeId: treeForSelectedNode?.src_ui_design ?? null,
setTreeOffset,
selectedNode: selectedNodeContext?.node ?? null,
selectedNodeId,
keepChildrenUnchanged,
@@ -1557,11 +1593,11 @@ export function useUiEditorSession(
selectedNode: selectedNodeContext?.node ?? null,
selectedNodeParentSize: selectedNodeContext?.parentSize,
selectedNodeParent:
selectedNodeId && treeForActiveImage
? (findUiNodeLocation(treeForActiveImage.root, selectedNodeId)
selectedNodeId && treeForSelectedNode
? (findUiNodeLocation(treeForSelectedNode.root, selectedNodeId)
?.parent ?? null)
: null,
tree: treeForActiveImage ?? null,
tree: treeForSelectedNode ?? treeForActiveImage ?? null,
previewUrls,
sprites,
fonts,