diff --git a/examples/vite-app/src/examples/Subflow/index.tsx b/examples/vite-app/src/examples/Subflow/index.tsx index 84003c83..8d2945e0 100644 --- a/examples/vite-app/src/examples/Subflow/index.tsx +++ b/examples/vite-app/src/examples/Subflow/index.tsx @@ -11,6 +11,7 @@ import ReactFlow, { Controls, MiniMap, Background, + NodeOrigin, } from 'reactflow'; import DebugNode from './DebugNode'; @@ -21,6 +22,7 @@ const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node); const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge); const defaultViewport = { x: 0, y: 0, zoom: 1.5 }; +const nodeOrigin: NodeOrigin = [0.5, 0.5]; const initialNodes: Node[] = [ { @@ -90,7 +92,7 @@ const initialNodes: Node[] = [ { id: '5a', data: { label: 'Node 5a' }, - position: { x: 25, y: 50 }, + position: { x: 0, y: 0 }, className: 'light', parentNode: '5', extent: 'parent', @@ -205,6 +207,7 @@ const Subflow = () => { onlyRenderVisibleElements={false} nodeTypes={nodeTypes} fitView + nodeOrigin={nodeOrigin} > diff --git a/packages/core/src/hooks/useDrag/index.ts b/packages/core/src/hooks/useDrag/index.ts index 319c1993..47c2113a 100644 --- a/packages/core/src/hooks/useDrag/index.ts +++ b/packages/core/src/hooks/useDrag/index.ts @@ -114,6 +114,7 @@ function useDrag({ onSelectionDrag, snapGrid, snapToGrid, + nodeOrigin, } = store.getState(); const pointerPos = getPointerPosition(event); // skip events without movement @@ -133,7 +134,7 @@ function useDrag({ nextPosition.y = snapGrid[1] * Math.round(nextPosition.y / snapGrid[1]); } - const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent); + const updatedPos = calcNextPosition(n, nextPosition, nodeInternals, nodeExtent, nodeOrigin); n.position = updatedPos.position; n.positionAbsolute = updatedPos.positionAbsolute; diff --git a/packages/core/src/hooks/useDrag/utils.ts b/packages/core/src/hooks/useDrag/utils.ts index be25f3c1..f9eb6ade 100644 --- a/packages/core/src/hooks/useDrag/utils.ts +++ b/packages/core/src/hooks/useDrag/utils.ts @@ -1,7 +1,8 @@ import type { RefObject } from 'react'; import { clampPosition, devWarn } from '../../utils'; -import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, XYPosition } from '../../types'; +import type { CoordinateExtent, Node, NodeDragItem, NodeInternals, NodeOrigin, XYPosition } from '../../types'; +import { getNodePosition } from '../../utils/graph'; export function isParentSelected(node: Node, nodeInternals: NodeInternals): boolean { if (!node.parentNode) { @@ -60,20 +61,22 @@ export function calcNextPosition( node: NodeDragItem | Node, nextPosition: XYPosition, nodeInternals: NodeInternals, - nodeExtent?: CoordinateExtent + nodeExtent?: CoordinateExtent, + nodeOrigin?: NodeOrigin ): { position: XYPosition; positionAbsolute: XYPosition } { let currentExtent = node.extent || nodeExtent; if (node.extent === 'parent') { if (node.parentNode && node.width && node.height) { const parent = nodeInternals.get(node.parentNode); + const parentPosition = getNodePosition(parent, nodeOrigin); currentExtent = - parent?.positionAbsolute && parent?.width && parent?.height + parentPosition.positionAbsolute && parent?.width && parent?.height ? [ - [parent.positionAbsolute.x, parent.positionAbsolute.y], + [parentPosition.positionAbsolute.x, parentPosition.positionAbsolute.y], [ - parent.positionAbsolute.x + parent.width - node.width, - parent.positionAbsolute.y + parent.height - node.height, + parentPosition.positionAbsolute.x + parent.width - node.width, + parentPosition.positionAbsolute.y + parent.height - node.height, ], ] : currentExtent; @@ -84,8 +87,8 @@ export function calcNextPosition( } } else if (node.extent && node.parentNode) { const parent = nodeInternals.get(node.parentNode); - const parentX = parent?.positionAbsolute?.x ?? 0; - const parentY = parent?.positionAbsolute?.y ?? 0; + const parentPosition = getNodePosition(parent, nodeOrigin); + const { x: parentX, y: parentY } = parentPosition.positionAbsolute; currentExtent = [ [node.extent[0][0] + parentX, node.extent[0][1] + parentY], [node.extent[1][0] + parentX, node.extent[1][1] + parentY], @@ -96,7 +99,7 @@ export function calcNextPosition( if (node.parentNode) { const parentNode = nodeInternals.get(node.parentNode); - parentPosition = { x: parentNode?.positionAbsolute?.x ?? 0, y: parentNode?.positionAbsolute?.y ?? 0 }; + parentPosition = getNodePosition(parentNode, nodeOrigin).positionAbsolute; } const positionAbsolute = currentExtent diff --git a/packages/core/src/store/index.ts b/packages/core/src/store/index.ts index e09abbd7..5ffd64c3 100644 --- a/packages/core/src/store/index.ts +++ b/packages/core/src/store/index.ts @@ -23,7 +23,8 @@ const createRFStore = () => createStore((set, get) => ({ ...initialState, setNodes: (nodes: Node[]) => { - set({ nodeInternals: createNodeInternals(nodes, get().nodeInternals) }); + const { nodeInternals, nodeOrigin } = get(); + set({ nodeInternals: createNodeInternals(nodes, nodeInternals, nodeOrigin) }); }, setEdges: (edges: Edge[]) => { const { defaultEdgeOptions = {} } = get(); @@ -33,7 +34,7 @@ const createRFStore = () => const hasDefaultNodes = typeof nodes !== 'undefined'; const hasDefaultEdges = typeof edges !== 'undefined'; - const nodeInternals = hasDefaultNodes ? createNodeInternals(nodes, new Map()) : new Map(); + const nodeInternals = hasDefaultNodes ? createNodeInternals(nodes, new Map(), get().nodeOrigin) : new Map(); const nextEdges = hasDefaultEdges ? edges : []; set({ nodeInternals, edges: nextEdges, hasDefaultNodes, hasDefaultEdges }); @@ -102,7 +103,7 @@ const createRFStore = () => } }, updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged = true, dragging = false) => { - const { onNodesChange, nodeInternals, hasDefaultNodes } = get(); + const { onNodesChange, nodeInternals, hasDefaultNodes, nodeOrigin } = get(); if (hasDefaultNodes || onNodesChange) { const changes = nodeDragItems.map((node) => { @@ -123,7 +124,7 @@ const createRFStore = () => if (changes?.length) { if (hasDefaultNodes) { const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values())); - const nextNodeInternals = createNodeInternals(nodes, nodeInternals); + const nextNodeInternals = createNodeInternals(nodes, nodeInternals, nodeOrigin); set({ nodeInternals: nextNodeInternals }); } diff --git a/packages/core/src/store/utils.ts b/packages/core/src/store/utils.ts index 2af09d46..c1b4fd7e 100644 --- a/packages/core/src/store/utils.ts +++ b/packages/core/src/store/utils.ts @@ -2,7 +2,7 @@ import { zoomIdentity } from 'd3-zoom'; import type { StoreApi } from 'zustand'; import { internalsSymbol, isNumeric } from '../utils'; -import { getD3Transition, getRectOfNodes, getTransformForBounds } from '../utils/graph'; +import { getD3Transition, getRectOfNodes, getTransformForBounds, getNodePosition } from '../utils/graph'; import type { Edge, EdgeSelectionChange, @@ -12,6 +12,7 @@ import type { ReactFlowState, XYZPosition, FitViewOptions, + NodeOrigin, } from '../types'; type ParentNodes = Record; @@ -20,21 +21,33 @@ function calculateXYZPosition( node: Node, nodeInternals: NodeInternals, parentNodes: ParentNodes, - result: XYZPosition + result: XYZPosition, + nodeOrigin: NodeOrigin ): XYZPosition { if (!node.parentNode) { return result; } const parentNode = nodeInternals.get(node.parentNode)!; + const parentNodePosition = getNodePosition(parentNode, nodeOrigin); - return calculateXYZPosition(parentNode, nodeInternals, parentNodes, { - x: (result.x ?? 0) + (parentNode.position?.x ?? 0), - y: (result.y ?? 0) + (parentNode.position?.y ?? 0), - z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0, - }); + return calculateXYZPosition( + parentNode, + nodeInternals, + parentNodes, + { + x: (result.x ?? 0) + parentNodePosition.x, + y: (result.y ?? 0) + parentNodePosition.y, + z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0, + }, + nodeOrigin + ); } -export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals): NodeInternals { +export function createNodeInternals( + nodes: Node[], + nodeInternals: NodeInternals, + nodeOrigin: NodeOrigin +): NodeInternals { const nextNodeInternals = new Map(); const parentNodes: ParentNodes = {}; @@ -74,10 +87,16 @@ export function createNodeInternals(nodes: Node[], nodeInternals: NodeInternals) } if (node.parentNode || parentNodes[node.id]) { - const { x, y, z } = calculateXYZPosition(node, nextNodeInternals, parentNodes, { - ...node.position, - z: node[internalsSymbol]?.z ?? 0, - }); + const { x, y, z } = calculateXYZPosition( + node, + nextNodeInternals, + parentNodes, + { + ...node.position, + z: node[internalsSymbol]?.z ?? 0, + }, + nodeOrigin + ); node.positionAbsolute = { x, diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index 0408603f..d6a4d068 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -141,23 +141,55 @@ export const pointToRendererPoint = ( return position; }; +export const getNodePosition = ( + node: Node | undefined, + nodeOrigin: NodeOrigin = [0, 0] +): XYPosition & { positionAbsolute: XYPosition } => { + if (!node) { + return { + x: 0, + y: 0, + positionAbsolute: { + x: 0, + y: 0, + }, + }; + } + + const position: XYPosition = { + x: node.position.x - (node.width ?? 0) * nodeOrigin[0], + y: node.position.y - (node.height ?? 0) * nodeOrigin[1], + }; + + const positionAbsolute: XYPosition = { + x: (node.positionAbsolute?.x ?? 0) - (node.width ?? 0) * nodeOrigin[0], + y: (node.positionAbsolute?.y ?? 0) - (node.height ?? 0) * nodeOrigin[1], + }; + + return { + ...position, + positionAbsolute, + }; +}; + export const getRectOfNodes = (nodes: Node[], nodeOrigin: NodeOrigin = [0, 0]): Rect => { if (nodes.length === 0) { return { x: 0, y: 0, width: 0, height: 0 }; } const box = nodes.reduce( - (currBox, { positionAbsolute, position, width, height }) => { + (currBox, node) => { + const { positionAbsolute, ...position } = getNodePosition(node, nodeOrigin); const nodeX = positionAbsolute ? positionAbsolute.x : position.x; const nodeY = positionAbsolute ? positionAbsolute.y : position.y; return getBoundsOfBoxes( currBox, rectToBox({ - x: nodeX - nodeOrigin[0] * (width || 0), - y: nodeY - nodeOrigin[1] * (height || 0), - width: width || 0, - height: height || 0, + x: nodeX, + y: nodeY, + width: node.width || 0, + height: node.height || 0, }) ); }, @@ -186,15 +218,17 @@ export const getNodesInside = ( const visibleNodes: Node[] = []; nodeInternals.forEach((node) => { - const { width, height, selectable = true, positionAbsolute = { x: 0, y: 0 } } = node; + const { width, height, selectable = true } = node; if (excludeNonSelectableNodes && !selectable) { return false; } + const { positionAbsolute } = getNodePosition(node, nodeOrigin); + const nodeRect = { - x: positionAbsolute.x - nodeOrigin[0] * (width || 0), - y: positionAbsolute.y - nodeOrigin[1] * (height || 0), + x: positionAbsolute.x, + y: positionAbsolute.y, width: width || 0, height: height || 0, }; @@ -243,4 +277,3 @@ export const getTransformForBounds = ( export const getD3Transition = (selection: D3Selection, duration = 0) => { return selection.transition().duration(duration); }; - diff --git a/packages/minimap/src/MiniMap.tsx b/packages/minimap/src/MiniMap.tsx index 4764a987..8d804f4e 100644 --- a/packages/minimap/src/MiniMap.tsx +++ b/packages/minimap/src/MiniMap.tsx @@ -162,6 +162,7 @@ function MiniMap({ {nodes.map((node) => (