UI编辑器重构多树预览与独立开关
预览画布同时渲染全部界面树并支持树级拖动 拆分显示框线、原图和组件开关 预览节点选择与左侧当前图片解耦
This commit is contained in:
@@ -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)
|
||||
|
||||
+171
-93
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user