调整 UI 编辑器节点变换为松开时提交
Project CI / Repository checks (pull_request) Failing after 16s
Project CI / Frontend tests (pull_request) Failing after 2m3s
Project CI / Backend tests (pull_request) Failing after 20s
Project CI / Native shell tests (pull_request) Failing after 4m30s

拖动和缩放期间仅更新预览临时变换

指针松开时提交最终变换,取消操作不写入 State

补充 UI 编辑器拖动变换提交边界文档
This commit is contained in:
2026-09-03 10:51:52 +08:00
parent 422c8931d6
commit 8a18b1b181
4 changed files with 73 additions and 12 deletions
@@ -24,6 +24,7 @@ import {
} from 'react';
import { findNodePageContext } from '../../../../features/ui-editor/nodeTransformGeometry';
import type { Node } from '../../../../features/ui-editor/types/Node';
import type { NodeId } from '../../../../features/ui-editor/types/NodeId';
import type { UiEditorCanvasProjection } from '../../useUiEditorPage';
import { UiNodeContextMenu } from '../UiNodeContextMenu';
@@ -48,6 +49,9 @@ export function PreviewWorkspace({
const [renderMode, setRenderMode] =
useState<UiEditorRenderMode>('editor-overlay');
const [showFrame, setShowFrame] = useState(false);
const [previewTransforms, setPreviewTransforms] = useState<
ReadonlyMap<NodeId, Node['layout']['transform']>
>(new Map());
const [contextMenu, setContextMenu] = useState<{
nodeId: NodeId;
x: number;
@@ -55,6 +59,20 @@ export function PreviewWorkspace({
isPageRoot: boolean;
} | null>(null);
const tree = canvas.tree ?? null;
const updatePreviewTransform = useCallback(
(nodeId: NodeId, transform: Node['layout']['transform'] | null) => {
setPreviewTransforms((current) => {
const next = new Map(current);
if (transform) next.set(nodeId, transform);
else next.delete(nodeId);
return next;
});
},
[],
);
useEffect(() => {
setPreviewTransforms(new Map());
}, [activeImageId]);
const activeImagePixelWidth = activeImage?.pixel_size[0];
const activeImagePixelHeight = activeImage?.pixel_size[1];
const activeImagePixelsPerUnit = activeImage?.pixels_per_unit;
@@ -88,6 +106,7 @@ export function PreviewWorkspace({
spaceHeld,
tree,
viewportRef,
onPreviewTransform: updatePreviewTransform,
});
const {
onNodePointerDown,
@@ -382,6 +401,7 @@ export function PreviewWorkspace({
renderMode={renderMode}
showFrame={showFrame}
hiddenNodeIds={canvas.hiddenNodeIds}
previewTransforms={previewTransforms}
selectedNodeId={canvas.selectedNodeId}
resources={{
previewUrls,
@@ -32,6 +32,7 @@ type UiTreeRendererProps = {
renderMode: UiEditorRenderMode;
showFrame: boolean;
hiddenNodeIds: ReadonlySet<NodeId>;
previewTransforms?: ReadonlyMap<NodeId, UiNode['layout']['transform']>;
selectedNodeId: NodeId | null;
resources: PreviewComponentResources;
onSelectNode: (id: NodeId) => void;
@@ -77,6 +78,7 @@ function RenderNode({
renderMode,
showFrame,
hiddenNodeIds,
previewTransforms,
selectedNodeId,
resources,
onSelectNode,
@@ -94,14 +96,15 @@ function RenderNode({
isRoot?: boolean;
parentContainer?: UiNode['layout']['container'];
}) {
const activePreviewTransforms = previewTransforms ?? new Map();
if (hiddenNodeIds.has(node.id)) return null;
const layout = activePreviewTransforms.has(node.id)
? { ...node.layout, transform: activePreviewTransforms.get(node.id)! }
: node.layout;
let geometry;
try {
geometry = controlLayoutToPreviewCss(
node.layout,
parentContainer !== undefined,
);
geometry = controlLayoutToPreviewCss(layout, parentContainer !== undefined);
} catch {
// Keep malformed nodes isolated from the rest of the tree.
return null;
@@ -120,7 +123,7 @@ function RenderNode({
style={{
...geometry,
...(parentContainer
? childInContainerToPreviewCss(node.layout, parentContainer)
? childInContainerToPreviewCss(layout, parentContainer)
: {}),
...containerToPreviewCss(node.layout.container),
...(isFrameVisible
@@ -189,6 +192,7 @@ function RenderNode({
renderMode={renderMode}
showFrame={showFrame}
hiddenNodeIds={hiddenNodeIds}
previewTransforms={activePreviewTransforms}
selectedNodeId={selectedNodeId}
resources={resources}
onSelectNode={onSelectNode}
@@ -29,6 +29,7 @@ type GestureBase = {
startClientY: number;
startTransform: UiNode['layout']['transform'];
hasMoved: boolean;
pendingTransform?: UiNode['layout']['transform'];
};
type ActiveGesture =
@@ -85,6 +86,7 @@ export function useNodeTransformInteraction({
spaceHeld,
tree,
viewportRef,
onPreviewTransform,
}: {
activeImageId: UiEditorCanvasProjection['activeImageId'];
canvas: Pick<UiEditorCanvasProjection, 'selectNode' | 'updateNodeTransform'>;
@@ -92,14 +94,21 @@ export function useNodeTransformInteraction({
spaceHeld: boolean;
tree: UITree | null;
viewportRef: RefObject<ViewportScale>;
onPreviewTransform?: (
nodeId: string,
transform: UiNode['layout']['transform'] | null,
) => void;
}) {
const activeGestureRef = useRef<ActiveGesture | null>(null);
const cancelGesture = useCallback(() => {
const gesture = activeGestureRef.current;
if (gesture) releasePointer(gesture.target, gesture.pointerId);
if (gesture) {
releasePointer(gesture.target, gesture.pointerId);
onPreviewTransform?.(gesture.nodeId, null);
}
activeGestureRef.current = null;
}, []);
}, [onPreviewTransform]);
useEffect(() => cancelGesture, [cancelGesture]);
@@ -185,18 +194,31 @@ export function useNodeTransformInteraction({
cancelGesture();
return;
}
canvas.updateNodeTransform(gesture.treeId, gesture.nodeId, nextTransform);
gesture.pendingTransform = nextTransform;
onPreviewTransform?.(gesture.nodeId, nextTransform);
},
[acceptsGestureEvent, cancelGesture, canvas, viewportRef],
[acceptsGestureEvent, cancelGesture, onPreviewTransform, viewportRef],
);
const onNodePointerUp = useCallback(
(event: ReactPointerEvent<HTMLDivElement>) => {
if (!acceptsGestureEvent(event)) return;
event.stopPropagation();
const gesture = activeGestureRef.current;
if (
event.type !== 'pointercancel' &&
gesture?.hasMoved &&
gesture.pendingTransform
) {
canvas.updateNodeTransform(
gesture.treeId,
gesture.nodeId,
gesture.pendingTransform,
);
}
cancelGesture();
},
[acceptsGestureEvent, cancelGesture],
[acceptsGestureEvent, cancelGesture, canvas],
);
const onNodeResizePointerDown = useCallback(
@@ -310,9 +332,10 @@ export function useNodeTransformInteraction({
return;
}
gesture.hasMoved = true;
canvas.updateNodeTransform(gesture.treeId, gesture.nodeId, nextTransform);
gesture.pendingTransform = nextTransform;
onPreviewTransform?.(gesture.nodeId, nextTransform);
},
[acceptsGestureEvent, cancelGesture, canvas, viewportRef],
[acceptsGestureEvent, cancelGesture, onPreviewTransform, viewportRef],
);
return {