统一节点变换实时预览
预览拖动和缩放时同步解析保持子节点页面矩形的变换 将检查器的子节点保持选项传入预览交互并补充回归测试
This commit is contained in:
@@ -107,6 +107,7 @@ export function PreviewWorkspace({
|
||||
logicalSize,
|
||||
spaceHeld,
|
||||
tree,
|
||||
keepChildrenUnchanged: canvas.keepChildrenUnchanged,
|
||||
viewportRef,
|
||||
onPreviewTransform: updatePreviewTransform,
|
||||
});
|
||||
|
||||
+127
-6
@@ -9,9 +9,12 @@ import {
|
||||
import {
|
||||
findNodePageContext,
|
||||
type PageRect,
|
||||
pageRectFromSize,
|
||||
type ResizeAxis,
|
||||
type ResizeHandle,
|
||||
resizePageRect,
|
||||
resolveChildrenTransformsForParentRect,
|
||||
resolvePageRect,
|
||||
resolveProportionalResizeAxis,
|
||||
setOffsetsForPageRect,
|
||||
} from '../../../../features/ui-editor/nodeTransformGeometry';
|
||||
@@ -30,6 +33,7 @@ type GestureBase = {
|
||||
startTransform: UiNode['layout']['transform'];
|
||||
hasMoved: boolean;
|
||||
pendingTransform?: UiNode['layout']['transform'];
|
||||
previewNodeIds: string[];
|
||||
};
|
||||
|
||||
type ActiveGesture =
|
||||
@@ -79,12 +83,69 @@ function releasePointer(target: HTMLDivElement, pointerId: number) {
|
||||
}
|
||||
}
|
||||
|
||||
function previewNodeIds(
|
||||
tree: UITree | null,
|
||||
nodeId: string,
|
||||
logicalSize: { width: number; height: number } | null,
|
||||
keepChildrenUnchanged: boolean,
|
||||
) {
|
||||
if (!keepChildrenUnchanged || !tree || !logicalSize) return [nodeId];
|
||||
const context = findNodePageContext(
|
||||
tree.root,
|
||||
nodeId,
|
||||
pageRectFromSize([logicalSize.width, logicalSize.height]),
|
||||
);
|
||||
return context
|
||||
? [nodeId, ...context.node.children.map((child) => child.id)]
|
||||
: [nodeId];
|
||||
}
|
||||
|
||||
function emitPreviewTransforms(
|
||||
gesture: ActiveGesture,
|
||||
tree: UITree | null,
|
||||
logicalSize: { width: number; height: number } | null,
|
||||
keepChildrenUnchanged: boolean,
|
||||
onPreviewTransform: (
|
||||
nodeId: string,
|
||||
transform: UiNode['layout']['transform'] | null,
|
||||
) => void,
|
||||
) {
|
||||
onPreviewTransform(gesture.nodeId, gesture.pendingTransform ?? null);
|
||||
if (
|
||||
!gesture.pendingTransform ||
|
||||
!keepChildrenUnchanged ||
|
||||
!tree ||
|
||||
!logicalSize
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const context = findNodePageContext(
|
||||
tree.root,
|
||||
gesture.nodeId,
|
||||
pageRectFromSize([logicalSize.width, logicalSize.height]),
|
||||
);
|
||||
if (!context) return;
|
||||
const newNodeRect = resolvePageRect(
|
||||
gesture.pendingTransform,
|
||||
context.parentRect,
|
||||
);
|
||||
if (!isFiniteRect(newNodeRect)) return;
|
||||
for (const child of resolveChildrenTransformsForParentRect(
|
||||
context.node.children,
|
||||
context.rect,
|
||||
newNodeRect,
|
||||
)) {
|
||||
onPreviewTransform(child.id, child.transform);
|
||||
}
|
||||
}
|
||||
|
||||
export function useNodeTransformInteraction({
|
||||
activeImageId,
|
||||
canvas,
|
||||
logicalSize,
|
||||
spaceHeld,
|
||||
tree,
|
||||
keepChildrenUnchanged,
|
||||
viewportRef,
|
||||
onPreviewTransform,
|
||||
}: {
|
||||
@@ -93,6 +154,7 @@ export function useNodeTransformInteraction({
|
||||
logicalSize: { width: number; height: number } | null;
|
||||
spaceHeld: boolean;
|
||||
tree: UITree | null;
|
||||
keepChildrenUnchanged: boolean;
|
||||
viewportRef: RefObject<ViewportScale>;
|
||||
onPreviewTransform?: (
|
||||
nodeId: string,
|
||||
@@ -109,6 +171,11 @@ export function useNodeTransformInteraction({
|
||||
if (gesture) {
|
||||
releasePointer(gesture.target, gesture.pointerId);
|
||||
onPreviewTransformRef.current?.(gesture.nodeId, null);
|
||||
for (const nodeId of gesture.previewNodeIds) {
|
||||
if (nodeId !== gesture.nodeId) {
|
||||
onPreviewTransformRef.current?.(nodeId, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
activeGestureRef.current = null;
|
||||
}, []);
|
||||
@@ -163,9 +230,22 @@ export function useNodeTransformInteraction({
|
||||
startClientY: event.clientY,
|
||||
startTransform: structuredClone(node.layout.transform),
|
||||
hasMoved: false,
|
||||
previewNodeIds: previewNodeIds(
|
||||
tree,
|
||||
node.id,
|
||||
logicalSize,
|
||||
keepChildrenUnchanged,
|
||||
),
|
||||
};
|
||||
},
|
||||
[activeImageId, canvas, spaceHeld, tree?.root.id],
|
||||
[
|
||||
activeImageId,
|
||||
canvas,
|
||||
keepChildrenUnchanged,
|
||||
logicalSize,
|
||||
spaceHeld,
|
||||
tree,
|
||||
],
|
||||
);
|
||||
|
||||
const onNodePointerMove = useCallback(
|
||||
@@ -198,9 +278,23 @@ export function useNodeTransformInteraction({
|
||||
return;
|
||||
}
|
||||
gesture.pendingTransform = nextTransform;
|
||||
onPreviewTransform?.(gesture.nodeId, nextTransform);
|
||||
emitPreviewTransforms(
|
||||
gesture,
|
||||
tree,
|
||||
logicalSize,
|
||||
keepChildrenUnchanged,
|
||||
(nodeId, transform) =>
|
||||
onPreviewTransformRef.current?.(nodeId, transform),
|
||||
);
|
||||
},
|
||||
[acceptsGestureEvent, cancelGesture, onPreviewTransform, viewportRef],
|
||||
[
|
||||
acceptsGestureEvent,
|
||||
cancelGesture,
|
||||
keepChildrenUnchanged,
|
||||
logicalSize,
|
||||
tree,
|
||||
viewportRef,
|
||||
],
|
||||
);
|
||||
|
||||
const onNodePointerUp = useCallback(
|
||||
@@ -276,9 +370,22 @@ export function useNodeTransformInteraction({
|
||||
parentRect: context.parentRect,
|
||||
ratioAxis: null,
|
||||
hasMoved: false,
|
||||
previewNodeIds: previewNodeIds(
|
||||
tree,
|
||||
node.id,
|
||||
logicalSize,
|
||||
keepChildrenUnchanged,
|
||||
),
|
||||
};
|
||||
},
|
||||
[activeImageId, canvas, logicalSize, spaceHeld, tree],
|
||||
[
|
||||
activeImageId,
|
||||
canvas,
|
||||
keepChildrenUnchanged,
|
||||
logicalSize,
|
||||
spaceHeld,
|
||||
tree,
|
||||
],
|
||||
);
|
||||
|
||||
const onNodeResizePointerMove = useCallback(
|
||||
@@ -336,9 +443,23 @@ export function useNodeTransformInteraction({
|
||||
}
|
||||
gesture.hasMoved = true;
|
||||
gesture.pendingTransform = nextTransform;
|
||||
onPreviewTransform?.(gesture.nodeId, nextTransform);
|
||||
emitPreviewTransforms(
|
||||
gesture,
|
||||
tree,
|
||||
logicalSize,
|
||||
keepChildrenUnchanged,
|
||||
(nodeId, transform) =>
|
||||
onPreviewTransformRef.current?.(nodeId, transform),
|
||||
);
|
||||
},
|
||||
[acceptsGestureEvent, cancelGesture, onPreviewTransform, viewportRef],
|
||||
[
|
||||
acceptsGestureEvent,
|
||||
cancelGesture,
|
||||
keepChildrenUnchanged,
|
||||
logicalSize,
|
||||
tree,
|
||||
viewportRef,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1214,6 +1214,7 @@ export function useUiEditorSession(
|
||||
tree: treeForActiveImage ?? null,
|
||||
selectedNode: selectedNodeContext?.node ?? null,
|
||||
selectedNodeId,
|
||||
keepChildrenUnchanged,
|
||||
hiddenNodeIds,
|
||||
focusRequest,
|
||||
status,
|
||||
|
||||
@@ -44,6 +44,17 @@ const pageTree: UITree = {
|
||||
root: node('root', [child]),
|
||||
};
|
||||
|
||||
function transformedNode(
|
||||
id: string,
|
||||
transform: UiNode['layout']['transform'],
|
||||
children: UiNode[] = [],
|
||||
) {
|
||||
return {
|
||||
...node(id, children),
|
||||
layout: { ...node(id).layout, transform },
|
||||
};
|
||||
}
|
||||
|
||||
function gestureTarget() {
|
||||
const target = document.createElement('div');
|
||||
Object.assign(target, {
|
||||
@@ -83,10 +94,17 @@ function renderInteraction({
|
||||
activeImageId = 'page',
|
||||
scale = 1,
|
||||
tree = pageTree,
|
||||
keepChildrenUnchanged = false,
|
||||
onPreviewTransform,
|
||||
}: {
|
||||
activeImageId?: string | null;
|
||||
scale?: number;
|
||||
tree?: UITree | null;
|
||||
keepChildrenUnchanged?: boolean;
|
||||
onPreviewTransform?: (
|
||||
nodeId: string,
|
||||
transform: UiNode['layout']['transform'] | null,
|
||||
) => void;
|
||||
} = {}) {
|
||||
const canvasProjection = canvas();
|
||||
const viewportRef = { current: { scale } };
|
||||
@@ -99,6 +117,8 @@ function renderInteraction({
|
||||
spaceHeld: false,
|
||||
tree: currentTree,
|
||||
viewportRef,
|
||||
keepChildrenUnchanged,
|
||||
onPreviewTransform,
|
||||
}),
|
||||
{ initialProps: { imageId: activeImageId, currentTree: tree } },
|
||||
);
|
||||
@@ -106,6 +126,97 @@ function renderInteraction({
|
||||
}
|
||||
|
||||
describe('useNodeTransformInteraction', () => {
|
||||
it('previews stable child page rectangles when the parent moves', () => {
|
||||
const nestedChild = transformedNode('child', {
|
||||
anchor_min: [0, 0],
|
||||
anchor_max: [0, 0],
|
||||
offset_min: [10, 10],
|
||||
offset_max: [60, 50],
|
||||
});
|
||||
const parent = transformedNode(
|
||||
'parent',
|
||||
{
|
||||
anchor_min: [0, 0],
|
||||
anchor_max: [0, 0],
|
||||
offset_min: [20, 20],
|
||||
offset_max: [120, 100],
|
||||
},
|
||||
[nestedChild],
|
||||
);
|
||||
const tree: UITree = {
|
||||
src_ui_design: 'page',
|
||||
root: node('root', [parent]),
|
||||
};
|
||||
const onPreviewTransform = vi.fn();
|
||||
const { result } = renderInteraction({
|
||||
tree,
|
||||
keepChildrenUnchanged: true,
|
||||
onPreviewTransform,
|
||||
});
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodePointerDown(pointerEvent(target, 1, 0, 0), parent);
|
||||
result.current.onNodePointerMove(pointerEvent(target, 1, 10, 5));
|
||||
});
|
||||
|
||||
expect(onPreviewTransform).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'parent',
|
||||
expect.objectContaining({ offset_min: [30, 25], offset_max: [130, 105] }),
|
||||
);
|
||||
expect(onPreviewTransform).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'child',
|
||||
expect.objectContaining({ offset_min: [0, 5], offset_max: [50, 45] }),
|
||||
);
|
||||
});
|
||||
|
||||
it('previews stable child page rectangles when the parent resizes', () => {
|
||||
const nestedChild = transformedNode('child', {
|
||||
anchor_min: [0, 0],
|
||||
anchor_max: [0, 0],
|
||||
offset_min: [10, 10],
|
||||
offset_max: [60, 50],
|
||||
});
|
||||
const parent = transformedNode(
|
||||
'parent',
|
||||
{
|
||||
anchor_min: [0, 0],
|
||||
anchor_max: [0, 0],
|
||||
offset_min: [20, 20],
|
||||
offset_max: [120, 100],
|
||||
},
|
||||
[nestedChild],
|
||||
);
|
||||
const tree: UITree = {
|
||||
src_ui_design: 'page',
|
||||
root: node('root', [parent]),
|
||||
};
|
||||
const onPreviewTransform = vi.fn();
|
||||
const { result } = renderInteraction({
|
||||
tree,
|
||||
keepChildrenUnchanged: true,
|
||||
onPreviewTransform,
|
||||
});
|
||||
const target = gestureTarget();
|
||||
|
||||
act(() => {
|
||||
result.current.onNodeResizePointerDown(
|
||||
pointerEvent(target, 1, 0, 0),
|
||||
parent,
|
||||
'nw',
|
||||
);
|
||||
result.current.onNodeResizePointerMove(pointerEvent(target, 1, 10, 5));
|
||||
});
|
||||
|
||||
expect(onPreviewTransform).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'child',
|
||||
expect.objectContaining({ offset_min: [0, 5], offset_max: [50, 45] }),
|
||||
);
|
||||
});
|
||||
|
||||
it('makes drag and resize mutually exclusive, then permits the next gesture', () => {
|
||||
const { result, canvasProjection } = renderInteraction();
|
||||
const dragTarget = gestureTarget();
|
||||
|
||||
Reference in New Issue
Block a user