修复多树预览受左侧选中图片限制

移除 activeImageId 对预览拖动和缩放手势的门控

按树 wrapper 提供几何上下文并允许任意树操作

保留 activeImageId 仅用于左侧图片与图片级检查
This commit is contained in:
2026-09-14 18:56:03 +08:00
parent 791794c0a6
commit 184d88dbb8
3 changed files with 69 additions and 89 deletions
@@ -30,6 +30,7 @@ import {
import { findNodePageContext } from '../../../../features/ui-editor/nodeTransformGeometry';
import type { Node } from '../../../../features/ui-editor/types/Node';
import type { Node as UiNode } from '../../../../features/ui-editor/types/Node';
import type { NodeId } from '../../../../features/ui-editor/types/NodeId';
import type { UiEditorCanvasProjection } from '../../useUiEditorPage';
import { UiNodeContextMenu } from '../UiNodeContextMenu';
@@ -68,8 +69,7 @@ export function PreviewWorkspace({
showOriginImage: boolean;
showComponent: boolean;
}) {
const { activeImage, activeImageId, previewUrls, uiTrees, images } = canvas;
const tree = canvas.tree ?? null;
const { previewUrls, uiTrees, images } = canvas;
const viewportElementRef = useRef<HTMLDivElement>(null);
const viewportRef = useRef<CanvasViewport>({ x: 0, y: 0, scale: 0.5 });
const handledFocusRequestIdRef = useRef<number | null>(null);
@@ -116,34 +116,6 @@ export function PreviewWorkspace({
},
[],
);
useEffect(() => {
setPreviewTransforms(new Map());
}, [activeImageId]);
const activeImagePixelWidth = activeImage?.pixel_size[0];
const activeImagePixelHeight = activeImage?.pixel_size[1];
const activeImagePixelsPerUnit = activeImage?.pixels_per_unit;
const logicalSize = useMemo(() => {
if (
activeImagePixelWidth === undefined ||
activeImagePixelHeight === undefined ||
activeImagePixelsPerUnit === undefined
) {
return null;
}
const ppu = activeImagePixelsPerUnit;
if (!Number.isFinite(ppu) || ppu <= 0) return null;
const width = activeImagePixelWidth / ppu;
const height = activeImagePixelHeight / ppu;
if (
!Number.isFinite(width) ||
!Number.isFinite(height) ||
width <= 0 ||
height <= 0
) {
return null;
}
return { width, height };
}, [activeImagePixelHeight, activeImagePixelsPerUnit, activeImagePixelWidth]);
const treeLayouts = useMemo(
() =>
uiTrees.map((item) => {
@@ -176,13 +148,22 @@ export function PreviewWorkspace({
);
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
}, [treeLayouts]);
const logicalSizes = useMemo(
() =>
new Map(
treeLayouts.map(
({ tree: item, width, height }) =>
[item.src_ui_design, { width, height }] as const,
),
),
[treeLayouts],
);
const nodeInteractions = useNodeTransformInteraction({
activeImageId,
canvas,
logicalSize,
trees: uiTrees,
logicalSizes,
spaceHeld,
tree,
keepChildrenUnchanged: canvas.keepChildrenUnchanged,
viewportRef,
onPreviewTransform: updatePreviewTransform,
@@ -285,15 +266,12 @@ export function PreviewWorkspace({
useEffect(() => {
const request = canvas.focusRequest;
if (
!request ||
request.requestId === handledFocusRequestIdRef.current ||
request.treeId !== activeImageId ||
!tree ||
!logicalSize
) {
if (!request || request.requestId === handledFocusRequestIdRef.current) {
return;
}
const tree = uiTrees.find((item) => item.src_ui_design === request.treeId);
const logicalSize = logicalSizes.get(request.treeId);
if (!tree || !logicalSize) return;
const context = findNodePageContext(tree.root, request.nodeId, {
min: [0, 0],
max: [logicalSize.width, logicalSize.height],
@@ -317,13 +295,12 @@ export function PreviewWorkspace({
),
);
}, [
activeImageId,
canvasSize.height,
canvasSize.width,
canvas.focusRequest,
logicalSize,
logicalSizes,
setViewport,
tree,
uiTrees,
]);
useEffect(() => {
@@ -350,7 +327,7 @@ export function PreviewWorkspace({
handlePreviewZoomKeyDown(
event,
{
hasZoomableViewport: logicalSize !== null,
hasZoomableViewport: allBounds !== null,
isFocused: previewFocusedRef.current,
isHovered: previewHoveredRef.current,
usesMetaModifier,
@@ -374,7 +351,7 @@ export function PreviewWorkspace({
window.removeEventListener('keyup', onKeyUp);
window.removeEventListener('blur', onWindowBlur);
};
}, [logicalSize, resetToActualSize, zoomIn, zoomOut]);
}, [allBounds, resetToActualSize, zoomIn, zoomOut]);
const handlePointerDown = (event: ReactPointerEvent<HTMLDivElement>) => {
if (event.button === 0 && !isPreviewZoomInteractiveTarget(event.target)) {
@@ -479,7 +456,7 @@ export function PreviewWorkspace({
const handleNodeContextMenu = (
event: ReactMouseEvent<HTMLDivElement>,
node: NonNullable<typeof tree>['root'],
node: UiNode,
isPageRoot: boolean,
treeId?: string,
) => {
@@ -23,6 +23,7 @@ export type { ResizeHandle } from '../../../../features/ui-editor/nodeTransformG
type NodePointerDown = (
event: ReactPointerEvent<HTMLDivElement>,
node: UiNode,
treeId?: string,
) => void;
type UiTreeRendererProps = {
@@ -50,6 +51,7 @@ type UiTreeRendererProps = {
event: ReactPointerEvent<HTMLDivElement>,
node: UiNode,
handle: ResizeHandle,
treeId?: string,
) => void;
onNodeResizePointerMove: (event: ReactPointerEvent<HTMLDivElement>) => void;
onNodeResizePointerUp: (event: ReactPointerEvent<HTMLDivElement>) => void;
@@ -178,7 +180,7 @@ function RenderNode({
}}
onPointerDown={
receivesPointerGesture
? (event) => onNodePointerDown(event, node)
? (event) => onNodePointerDown(event, node, treeId)
: undefined
}
onPointerMove={receivesPointerGesture ? onNodePointerMove : undefined}
@@ -255,7 +257,7 @@ function RenderNode({
touchAction: 'none',
}}
onPointerDown={(event) =>
onNodeResizePointerDown(event, node, handle.id)
onNodeResizePointerDown(event, node, handle.id, treeId)
}
onPointerMove={onNodeResizePointerMove}
onPointerUp={onNodeResizePointerUp}
@@ -176,22 +176,20 @@ function emitPreviewTransforms(
}
export function useNodeTransformInteraction({
activeImageId,
canvas,
logicalSize,
trees,
logicalSizes,
spaceHeld,
tree,
keepChildrenUnchanged,
viewportRef,
onPreviewTransform,
previewRef,
selectedNodeId,
}: {
activeImageId: UiEditorCanvasProjection['activeImageId'];
canvas: Pick<UiEditorCanvasProjection, 'selectNode' | 'updateNodeTransform'>;
logicalSize: { width: number; height: number } | null;
trees: readonly UITree[];
logicalSizes: ReadonlyMap<string, { width: number; height: number }>;
spaceHeld: boolean;
tree: UITree | null;
keepChildrenUnchanged: boolean;
viewportRef: RefObject<ViewportScale>;
onPreviewTransform?: (
@@ -229,36 +227,30 @@ export function useNodeTransformInteraction({
return () => window.removeEventListener('blur', cancelGesture);
}, [cancelGesture]);
useEffect(() => {
const gesture = activeGestureRef.current;
if (
gesture &&
(gesture.treeId !== activeImageId ||
tree?.src_ui_design !== gesture.treeId)
) {
cancelGesture();
}
}, [activeImageId, cancelGesture, tree]);
const acceptsGestureEvent = useCallback(
(event: ReactPointerEvent<HTMLDivElement>) => {
const gesture = activeGestureRef.current;
return (
gesture !== null &&
gesture.pointerId === event.pointerId &&
gesture.treeId === activeImageId
);
return gesture !== null && gesture.pointerId === event.pointerId;
},
[activeImageId],
[],
);
const onNodePointerDown = useCallback(
(event: ReactPointerEvent<HTMLDivElement>, node: UiNode) => {
(
event: ReactPointerEvent<HTMLDivElement>,
node: UiNode,
treeId?: string,
) => {
const tree = treeId
? (trees.find((candidate) => candidate.src_ui_design === treeId) ??
null)
: null;
const logicalSize = treeId ? (logicalSizes.get(treeId) ?? null) : null;
if (
activeGestureRef.current !== null ||
event.button !== 0 ||
spaceHeld ||
!activeImageId ||
!treeId ||
node.id === tree?.root.id ||
!isFiniteTransform(node.layout.transform)
) {
@@ -275,7 +267,7 @@ export function useNodeTransformInteraction({
event.preventDefault();
activeGestureRef.current = {
kind: 'drag',
treeId: activeImageId,
treeId,
nodeId: dragNode.id,
pointerId: event.pointerId,
target: event.currentTarget,
@@ -292,14 +284,7 @@ export function useNodeTransformInteraction({
),
};
},
[
activeImageId,
keepChildrenUnchanged,
logicalSize,
spaceHeld,
selectedNodeId,
tree,
],
[keepChildrenUnchanged, logicalSizes, spaceHeld, selectedNodeId, trees],
);
const onNodePointerMove = useCallback(
@@ -309,6 +294,11 @@ export function useNodeTransformInteraction({
return;
}
event.stopPropagation();
const tree =
trees.find((candidate) => candidate.src_ui_design === gesture.treeId) ??
null;
const logicalSize = logicalSizes.get(gesture.treeId) ?? null;
if (!tree || !logicalSize) return;
if (!gesture.hasMoved && !passedDragThreshold(gesture, event)) return;
const scale = viewportRef.current?.scale;
if (!Number.isFinite(scale) || scale <= 0) {
@@ -346,8 +336,8 @@ export function useNodeTransformInteraction({
acceptsGestureEvent,
cancelGesture,
keepChildrenUnchanged,
logicalSize,
tree,
logicalSizes,
trees,
viewportRef,
],
);
@@ -389,12 +379,18 @@ export function useNodeTransformInteraction({
event: ReactPointerEvent<HTMLDivElement>,
node: UiNode,
handle: ResizeHandle,
treeId?: string,
) => {
const tree = treeId
? (trees.find((candidate) => candidate.src_ui_design === treeId) ??
null)
: null;
const logicalSize = treeId ? (logicalSizes.get(treeId) ?? null) : null;
if (
activeGestureRef.current !== null ||
event.button !== 0 ||
spaceHeld ||
!activeImageId ||
!treeId ||
node.id === tree?.root.id ||
!tree ||
!logicalSize ||
@@ -424,7 +420,7 @@ export function useNodeTransformInteraction({
suppressNextNodeClickRef.current = false;
activeGestureRef.current = {
kind: 'resize',
treeId: activeImageId,
treeId,
nodeId: node.id,
pointerId: event.pointerId,
target: event.currentTarget,
@@ -445,7 +441,7 @@ export function useNodeTransformInteraction({
),
};
},
[activeImageId, keepChildrenUnchanged, logicalSize, spaceHeld, tree],
[keepChildrenUnchanged, logicalSizes, spaceHeld, trees],
);
const onNodeResizePointerMove = useCallback(
@@ -459,6 +455,11 @@ export function useNodeTransformInteraction({
return;
}
event.stopPropagation();
const tree =
trees.find((candidate) => candidate.src_ui_design === gesture.treeId) ??
null;
const logicalSize = logicalSizes.get(gesture.treeId) ?? null;
if (!tree || !logicalSize) return;
if (!gesture.hasMoved && !passedDragThreshold(gesture, event)) return;
const scale = viewportRef.current?.scale;
if (!Number.isFinite(scale) || scale <= 0) {
@@ -517,8 +518,8 @@ export function useNodeTransformInteraction({
acceptsGestureEvent,
cancelGesture,
keepChildrenUnchanged,
logicalSize,
tree,
logicalSizes,
trees,
viewportRef,
],
);