From d6b8439f6ec1f890722db414dd7051b0c0b337f9 Mon Sep 17 00:00:00 2001 From: moklick Date: Mon, 12 Jun 2023 22:32:57 +0200 Subject: [PATCH] feat(utils): add snapPosition function --- .../react/src/hooks/useGlobalKeyHandler.ts | 4 +- packages/react/src/hooks/useResizeHandler.ts | 36 +++++++---------- .../react/src/hooks/useUpdateNodePositions.ts | 40 +++++++++++-------- packages/system/src/utils/graph.ts | 28 +++++++------ packages/system/src/xydrag/XYDrag.ts | 9 ++--- 5 files changed, 60 insertions(+), 57 deletions(-) diff --git a/packages/react/src/hooks/useGlobalKeyHandler.ts b/packages/react/src/hooks/useGlobalKeyHandler.ts index 5d3c5bcd..79b49a3f 100644 --- a/packages/react/src/hooks/useGlobalKeyHandler.ts +++ b/packages/react/src/hooks/useGlobalKeyHandler.ts @@ -6,7 +6,7 @@ import useKeyPress from './useKeyPress'; import useReactFlow from './useReactFlow'; import { Edge, Node } from '../types'; -const getSelected = (item: Node | Edge) => item.selected; +const selected = (item: Node | Edge) => item.selected; export default ({ deleteKeyCode, @@ -24,7 +24,7 @@ export default ({ useEffect(() => { if (deleteKeyPressed) { const { edges, nodes } = store.getState(); - deleteElements({ nodes: nodes.filter(getSelected), edges: edges.filter(getSelected) }); + deleteElements({ nodes: nodes.filter(selected), edges: edges.filter(selected) }); store.setState({ nodesSelectionActive: false }); } }, [deleteKeyPressed]); diff --git a/packages/react/src/hooks/useResizeHandler.ts b/packages/react/src/hooks/useResizeHandler.ts index 863ee261..6d8433ef 100644 --- a/packages/react/src/hooks/useResizeHandler.ts +++ b/packages/react/src/hooks/useResizeHandler.ts @@ -3,18 +3,12 @@ import { errorMessages, getDimensions } from '@xyflow/system'; import { useStoreApi } from '../hooks/useStore'; -function useResizeHandler(rendererNode: MutableRefObject): void { +function useResizeHandler(domNode: MutableRefObject): void { const store = useStoreApi(); useEffect(() => { - let resizeObserver: ResizeObserver; - const updateDimensions = () => { - if (!rendererNode.current) { - return; - } - - const size = getDimensions(rendererNode.current); + const size = getDimensions(domNode.current!); if (size.height === 0 || size.width === 0) { store.getState().onError?.('004', errorMessages['error004']()); @@ -23,21 +17,21 @@ function useResizeHandler(rendererNode: MutableRefObject) store.setState({ width: size.width || 500, height: size.height || 500 }); }; - updateDimensions(); - window.addEventListener('resize', updateDimensions); + if (domNode.current) { + updateDimensions(); + window.addEventListener('resize', updateDimensions); - if (rendererNode.current) { - resizeObserver = new ResizeObserver(() => updateDimensions()); - resizeObserver.observe(rendererNode.current); + const resizeObserver = new ResizeObserver(() => updateDimensions()); + resizeObserver.observe(domNode.current); + + return () => { + window.removeEventListener('resize', updateDimensions); + + if (resizeObserver && domNode.current) { + resizeObserver.unobserve(domNode.current); + } + }; } - - return () => { - window.removeEventListener('resize', updateDimensions); - - if (resizeObserver && rendererNode.current) { - resizeObserver.unobserve(rendererNode.current!); - } - }; }, []); } diff --git a/packages/react/src/hooks/useUpdateNodePositions.ts b/packages/react/src/hooks/useUpdateNodePositions.ts index 91576b5f..d303fb9b 100644 --- a/packages/react/src/hooks/useUpdateNodePositions.ts +++ b/packages/react/src/hooks/useUpdateNodePositions.ts @@ -1,41 +1,49 @@ import { useCallback } from 'react'; -import { calcNextPosition } from '@xyflow/system'; +import { calcNextPosition, snapPosition } from '@xyflow/system'; +import { Node } from '../types'; import { useStoreApi } from '../hooks/useStore'; +const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) => + n.selected && (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')); + function useUpdateNodePositions() { const store = useStoreApi(); const updatePositions = useCallback((params: { x: number; y: number; isShiftPressed: boolean }) => { - const { nodeExtent, updateNodePositions, nodes, snapToGrid, snapGrid, onError, nodesDraggable } = store.getState(); - const selectedNodes = nodes.filter( - (n) => n.selected && (n.draggable || (nodesDraggable && typeof n.draggable === 'undefined')) - ); + const { nodeExtent, nodes, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions } = store.getState(); + const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable)); // by default a node moves 5px on each key press, or 20px if shift is pressed // if snap grid is enabled, we use that for the velocity. const xVelo = snapToGrid ? snapGrid[0] : 5; const yVelo = snapToGrid ? snapGrid[1] : 5; const factor = params.isShiftPressed ? 4 : 1; - const positionDiffX = params.x * xVelo * factor; - const positionDiffY = params.y * yVelo * factor; + const xDiff = params.x * xVelo * factor; + const yDiff = params.y * yVelo * factor; - const nodeUpdates = selectedNodes.map((n) => { - if (n.positionAbsolute) { - const nextPosition = { x: n.positionAbsolute.x + positionDiffX, y: n.positionAbsolute.y + positionDiffY }; + const nodeUpdates = selectedNodes.map((node) => { + if (node.positionAbsolute) { + let nextPosition = { x: node.positionAbsolute.x + xDiff, y: node.positionAbsolute.y + yDiff }; if (snapToGrid) { - nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]); - nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]); + nextPosition = snapPosition(nextPosition, snapGrid); } - const { positionAbsolute, position } = calcNextPosition(n, nextPosition, nodes, nodeExtent, undefined, onError); + const { positionAbsolute, position } = calcNextPosition( + node, + nextPosition, + nodes, + nodeExtent, + undefined, + onError + ); - n.position = position; - n.positionAbsolute = positionAbsolute; + node.position = position; + node.positionAbsolute = positionAbsolute; } - return n; + return node; }); updateNodePositions(nodeUpdates, true, false); diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 01360264..6fd345af 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -144,21 +144,14 @@ export const pointToRendererPoint = ( { x, y }: XYPosition, [tx, ty, tScale]: Transform, snapToGrid: boolean, - [snapX, snapY]: [number, number] + snapGrid: SnapGrid ): XYPosition => { const position: XYPosition = { x: (x - tx) / tScale, y: (y - ty) / tScale, }; - if (snapToGrid) { - return { - x: snapX * Math.round(position.x / snapX), - y: snapY * Math.round(position.y / snapY), - }; - } - - return position; + return snapToGrid ? snapPosition(position, snapGrid) : position; }; export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => { @@ -349,10 +342,12 @@ export function getPointerPosition( y: (y - transform[1]) / transform[2], }; + const { x: xSnapped, y: ySnapped } = snapToGrid ? snapPosition(pointerPos, snapGrid) : pointerPos; + // we need the snapped position in order to be able to skip unnecessary drag events return { - xSnapped: snapToGrid ? snapGrid[0] * Math.round(pointerPos.x / snapGrid[0]) : pointerPos.x, - ySnapped: snapToGrid ? snapGrid[1] * Math.round(pointerPos.y / snapGrid[1]) : pointerPos.y, + xSnapped, + ySnapped, ...pointerPos, }; } @@ -366,11 +361,11 @@ export function calcNextPosition( onError?: OnError ): { position: XYPosition; positionAbsolute: XYPosition } { let currentExtent = node.extent || nodeExtent; - let parentNode: NodeType; + let parentNode: NodeType | null = null; let parentPos = { x: 0, y: 0 }; if (node.parentNode) { - parentNode = nodes.find((n) => n.id === node.parentNode); + parentNode = nodes.find((n) => n.id === node.parentNode) || null; parentPos = parentNode ? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute : parentPos; @@ -413,3 +408,10 @@ export function calcNextPosition( positionAbsolute, }; } + +export function snapPosition(position: XYPosition, snapGrid: SnapGrid = [1, 1]) { + return { + x: snapGrid[0] * Math.round(position.x / snapGrid[0]), + y: snapGrid[1] * Math.round(position.y / snapGrid[1]), + }; +} diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index a3caded0..293e4b0e 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -1,7 +1,7 @@ import { drag } from 'd3-drag'; import { select } from 'd3-selection'; -import { calcAutoPan, getEventPosition, getPointerPosition, calcNextPosition } from '../utils'; +import { calcAutoPan, getEventPosition, getPointerPosition, calcNextPosition, snapPosition } from '../utils'; import { getDragItems, getEventHandlerParams, hasSelector, wrapSelectionDragFunc } from './utils'; import type { BaseNode, @@ -107,11 +107,10 @@ export function XYDrag({ let hasChange = false; dragItems = dragItems.map((n) => { - const nextPosition = { x: x - n.distance.x, y: y - n.distance.y }; + let nextPosition = { x: x - n.distance.x, y: y - n.distance.y }; if (snapToGrid) { - nextPosition.x = snapGrid[0] * Math.round(nextPosition.x / snapGrid[0]); - nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]); + nextPosition = snapPosition(nextPosition, snapGrid); } const updatedPos = calcNextPosition(n, nextPosition, nodes, nodeExtent, nodeOrigin, onError); @@ -173,10 +172,10 @@ export function XYDrag({ transform, snapGrid, snapToGrid, + selectNodesOnDrag, onNodeDragStart, onSelectionDragStart, unselectNodesAndEdges, - selectNodesOnDrag, } = getStoreItems(); if (!selectNodesOnDrag && !multiSelectionActive && nodeId) {