修正预览拖动使用当前选中节点
拖动手势优先更新按下前已选中的节点 补充跨节点拖动回归测试并同步交互约定
This commit is contained in:
@@ -88,6 +88,7 @@ export function PreviewWorkspace({
|
||||
spaceHeld,
|
||||
tree,
|
||||
previewRef: viewportElementRef,
|
||||
selectedNodeId: canvas.selectedNodeId,
|
||||
viewportRef,
|
||||
});
|
||||
const {
|
||||
|
||||
+19
-3
@@ -73,6 +73,15 @@ function isFiniteTransform(transform: UiNode['layout']['transform']) {
|
||||
);
|
||||
}
|
||||
|
||||
function findNodeById(node: UiNode, nodeId: string): UiNode | null {
|
||||
if (node.id === nodeId) return node;
|
||||
for (const child of node.children) {
|
||||
const match = findNodeById(child, nodeId);
|
||||
if (match) return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function releasePointer(target: HTMLDivElement, pointerId: number) {
|
||||
if (target.hasPointerCapture(pointerId)) {
|
||||
target.releasePointerCapture(pointerId);
|
||||
@@ -113,6 +122,7 @@ export function useNodeTransformInteraction({
|
||||
tree,
|
||||
viewportRef,
|
||||
previewRef,
|
||||
selectedNodeId,
|
||||
}: {
|
||||
activeImageId: UiEditorCanvasProjection['activeImageId'];
|
||||
canvas: Pick<UiEditorCanvasProjection, 'selectNode' | 'updateNodeTransform'>;
|
||||
@@ -121,6 +131,7 @@ export function useNodeTransformInteraction({
|
||||
tree: UITree | null;
|
||||
viewportRef: RefObject<ViewportScale>;
|
||||
previewRef?: RefObject<HTMLElement | null>;
|
||||
selectedNodeId?: string | null;
|
||||
}) {
|
||||
const activeGestureRef = useRef<ActiveGesture | null>(null);
|
||||
const suppressNextNodeClickRef = useRef(false);
|
||||
@@ -174,23 +185,28 @@ export function useNodeTransformInteraction({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const selectedNode =
|
||||
selectedNodeId && tree ? findNodeById(tree.root, selectedNodeId) : null;
|
||||
const dragNode =
|
||||
selectedNode && selectedNode.id !== tree?.root.id ? selectedNode : node;
|
||||
if (!isFiniteTransform(dragNode.layout.transform)) return;
|
||||
event.stopPropagation();
|
||||
event.currentTarget.setPointerCapture(event.pointerId);
|
||||
suppressNextNodeClickRef.current = false;
|
||||
activeGestureRef.current = {
|
||||
kind: 'drag',
|
||||
treeId: activeImageId,
|
||||
nodeId: node.id,
|
||||
nodeId: dragNode.id,
|
||||
pointerId: event.pointerId,
|
||||
target: event.currentTarget,
|
||||
startClientX: event.clientX,
|
||||
startClientY: event.clientY,
|
||||
startTransform: structuredClone(node.layout.transform),
|
||||
startTransform: structuredClone(dragNode.layout.transform),
|
||||
hitNodeId: resolveHitNodeId(event, node.id),
|
||||
hasMoved: false,
|
||||
};
|
||||
},
|
||||
[activeImageId, spaceHeld, tree?.root.id],
|
||||
[activeImageId, selectedNodeId, spaceHeld, tree],
|
||||
);
|
||||
|
||||
const onNodePointerMove = useCallback(
|
||||
|
||||
@@ -79,7 +79,10 @@ function canvas() {
|
||||
return {
|
||||
selectNode: vi.fn(),
|
||||
updateNodeTransform: vi.fn(),
|
||||
} as Pick<UiEditorCanvasProjection, 'selectNode' | 'updateNodeTransform'>;
|
||||
selectedNodeId: null as string | null,
|
||||
} as Pick<UiEditorCanvasProjection, 'selectNode' | 'updateNodeTransform'> & {
|
||||
selectedNodeId: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
function renderInteraction({
|
||||
@@ -87,13 +90,16 @@ function renderInteraction({
|
||||
scale = 1,
|
||||
tree = pageTree,
|
||||
previewRef,
|
||||
selectedNodeId = null,
|
||||
}: {
|
||||
activeImageId?: string | null;
|
||||
scale?: number;
|
||||
tree?: UITree | null;
|
||||
previewRef?: MutableRefObject<HTMLElement | null>;
|
||||
selectedNodeId?: string | null;
|
||||
} = {}) {
|
||||
const canvasProjection = canvas();
|
||||
canvasProjection.selectedNodeId = selectedNodeId;
|
||||
const viewportRef = { current: { scale } };
|
||||
const hook = renderHook(
|
||||
({ imageId, currentTree }) =>
|
||||
@@ -104,6 +110,7 @@ function renderInteraction({
|
||||
spaceHeld: false,
|
||||
tree: currentTree,
|
||||
previewRef,
|
||||
selectedNodeId,
|
||||
viewportRef,
|
||||
}),
|
||||
{ initialProps: { imageId: activeImageId, currentTree: tree } },
|
||||
@@ -112,6 +119,30 @@ function renderInteraction({
|
||||
}
|
||||
|
||||
describe('useNodeTransformInteraction', () => {
|
||||
it('moves the current selection when dragging another node', () => {
|
||||
const selected = node('selected');
|
||||
const treeWithSelection: UITree = {
|
||||
src_ui_design: 'page',
|
||||
root: node('root', [selected, child]),
|
||||
};
|
||||
const { result, canvasProjection } = renderInteraction({
|
||||
tree: treeWithSelection,
|
||||
selectedNodeId: 'selected',
|
||||
});
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerDown(pointerEvent(target, 1, 0, 0), child);
|
||||
result.current.onNodePointerMove(pointerEvent(target, 1, 4, 0));
|
||||
});
|
||||
|
||||
expect(canvasProjection.updateNodeTransform).toHaveBeenCalledWith(
|
||||
'page',
|
||||
'selected',
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('defers node selection until a clean pointerup', () => {
|
||||
const { result, canvasProjection } = renderInteraction();
|
||||
const target = gestureTarget();
|
||||
|
||||
@@ -41,4 +41,4 @@ Container 专属数据是互斥 tagged union:HBox/VBox 存 `alignment + separa
|
||||
|
||||
布局只在 Inspector 编辑。Preview 不为 Container 管理的 child 提供拖拽或缩放手柄;Inspector 会提示其 Transform 被父 Container 忽略。普通 Control 仍保留原 Transform 编辑和自由预览操作。
|
||||
|
||||
Preview 的节点选择采用“松开确认”语义:节点或调整手柄按下时只建立指针手势,不立即改变选择;沿用现有 `2px` 屏幕位移阈值(达到阈值即视为拖动),未达到阈值且在预览区域内松开时才选择按下命中的节点,发生拖动、取消、失焦或在区域外松开均不选择。拖动外层节点时,嵌套 child 产生的后续 click 也不得重新选择;未发生拖动时,嵌套 child 仍按实际命中节点选择。
|
||||
Preview 的节点选择采用“松开确认”语义:节点或调整手柄按下时只建立指针手势,不立即改变选择;沿用现有 `2px` 屏幕位移阈值(达到阈值即视为拖动),未达到阈值且在预览区域内松开时才选择按下命中的节点,发生拖动、取消、失焦或在区域外松开均不选择。发生拖动时,Transform 更新作用于按下前已选中的节点;没有当前选择时才回退到按下节点。拖动外层节点时,嵌套 child 产生的后续 click 也不得重新选择;未发生拖动时,嵌套 child 仍按实际命中节点选择。
|
||||
|
||||
Reference in New Issue
Block a user