diff --git a/.changeset/perfect-poems-tap.md b/.changeset/perfect-poems-tap.md new file mode 100644 index 00000000..698201c6 --- /dev/null +++ b/.changeset/perfect-poems-tap.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': patch +'@xyflow/svelte': patch +'@xyflow/system': patch +--- + +Snap selection instead of separate nodes when snap grid is enabled diff --git a/packages/system/src/xydrag/XYDrag.ts b/packages/system/src/xydrag/XYDrag.ts index ddd0d960..232fd574 100644 --- a/packages/system/src/xydrag/XYDrag.ts +++ b/packages/system/src/xydrag/XYDrag.ts @@ -10,7 +10,7 @@ import { getInternalNodesBounds, rectToBox, } from '../utils'; -import { getDragItems, getEventHandlerParams, hasSelector } from './utils'; +import { calculateSnapOffset, getDragItems, getEventHandlerParams, hasSelector } from './utils'; import type { NodeBase, NodeDragItem, @@ -25,7 +25,6 @@ import type { PanBy, OnSelectionDrag, UpdateNodePositions, - Box, InternalNodeBase, } from '../types'; @@ -132,39 +131,42 @@ export function XYDrag voi } = getStoreItems(); lastPos = { x, y }; - let hasChange = false; - let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 }; - if (dragItems.size > 1 && nodeExtent) { - const rect = getInternalNodesBounds(dragItems); - nodesBox = rectToBox(rect); - } + const isMultiDrag = dragItems.size > 1; + const nodesBox = isMultiDrag && nodeExtent ? rectToBox(getInternalNodesBounds(dragItems)) : null; + const multiDragSnapOffset = + isMultiDrag && snapToGrid + ? calculateSnapOffset({ + dragItems, + snapGrid, + x, + y, + }) + : null; for (const [id, dragItem] of dragItems) { + /* + * if the node is not in the nodeLookup anymore, it was probably deleted while dragging + */ if (!nodeLookup.has(id)) { - /* - * if the node is not in the nodeLookup anymore, it was probably deleted while dragging - * and we don't need to update it anymore - */ continue; } let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y }; + if (snapToGrid) { - nextPosition = snapPosition(nextPosition, snapGrid); + nextPosition = multiDragSnapOffset + ? { + x: nextPosition.x + multiDragSnapOffset.x, + y: nextPosition.y + multiDragSnapOffset.y, + } + : snapPosition(nextPosition, snapGrid); } - /* - * if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node - * based on its position so that the node stays at it's position relative to the selection. - */ - let adjustedNodeExtent: CoordinateExtent = [ - [nodeExtent[0][0], nodeExtent[0][1]], - [nodeExtent[1][0], nodeExtent[1][1]], - ]; + let adjustedNodeExtent: CoordinateExtent | null = null; - if (dragItems.size > 1 && nodeExtent && !dragItem.extent) { + if (isMultiDrag && nodeExtent && !dragItem.extent && nodesBox) { const { positionAbsolute } = dragItem.internals; const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0]; const x2 = positionAbsolute.x + dragItem.measured.width - nodesBox.x2 + nodeExtent[1][0]; @@ -182,7 +184,7 @@ export function XYDrag voi nodeId: id, nextPosition, nodeLookup, - nodeExtent: adjustedNodeExtent, + nodeExtent: adjustedNodeExtent ? adjustedNodeExtent : nodeExtent, nodeOrigin, onError, }); diff --git a/packages/system/src/xydrag/utils.ts b/packages/system/src/xydrag/utils.ts index dc980281..a62d7735 100644 --- a/packages/system/src/xydrag/utils.ts +++ b/packages/system/src/xydrag/utils.ts @@ -1,4 +1,5 @@ -import { type NodeDragItem, type XYPosition, InternalNodeBase, NodeBase, NodeLookup } from '../types'; +import { type NodeDragItem, type XYPosition, InternalNodeBase, NodeBase, NodeLookup, SnapGrid } from '../types'; +import { snapPosition } from '../utils'; export function isParentSelected(node: NodeType, nodeLookup: NodeLookup): boolean { if (!node.parentId) { @@ -114,10 +115,43 @@ export function getEventHandlerParams({ !node ? nodesFromDragItems[0] : { - ...node, - position: dragItems.get(nodeId)?.position || node.position, - dragging, - }, + ...node, + position: dragItems.get(nodeId)?.position || node.position, + dragging, + }, nodesFromDragItems, ]; } + +/** + * If a selection is being dragged we want to apply the same snap offset to all nodes in the selection. + * This function calculates the snap offset based on the first node in the selection. + */ +export function calculateSnapOffset({ + dragItems, + snapGrid, + x, + y, +}: { + dragItems: Map; + snapGrid: SnapGrid; + x: number; + y: number; +}) { + const refDragItem = dragItems.values().next().value; + + if (!refDragItem) { + return null; + } + + const refPos = { + x: x - refDragItem.distance.x, + y: y - refDragItem.distance.y, + }; + const refPosSnapped = snapPosition(refPos, snapGrid); + + return { + x: refPosSnapped.x - refPos.x, + y: refPosSnapped.y - refPos.y, + }; +}