diff --git a/examples/react/src/examples/NodeResizer/index.tsx b/examples/react/src/examples/NodeResizer/index.tsx index 8d5b7602..397c1f22 100644 --- a/examples/react/src/examples/NodeResizer/index.tsx +++ b/examples/react/src/examples/NodeResizer/index.tsx @@ -37,6 +37,7 @@ const initialNodes: Node[] = [ type: 'defaultResizer', data: { label: 'default resizer' }, position: { x: 0, y: 0 }, + origin: [1, 1], style: { ...nodeStyle }, }, { @@ -127,7 +128,7 @@ const initialNodes: Node[] = [ data: { label: 'Parent', keepAspectRatio: true }, position: { x: 700, y: 0 }, width: 300, - height: 400, + height: 300, style: { ...nodeStyle }, }, { @@ -147,7 +148,9 @@ const initialNodes: Node[] = [ id: '5b', type: 'defaultResizer', data: { label: 'Child with expandParent' }, - position: { x: 150, y: 100 }, + position: { x: 100, y: 100 }, + width: 100, + height: 100, parentId: '5', expandParent: true, style: { ...nodeStyle }, @@ -155,8 +158,10 @@ const initialNodes: Node[] = [ { id: '5c', type: 'defaultResizer', - data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true }, - position: { x: 25, y: 200 }, + data: { label: 'Child with expandParent & keepAspectRatio' }, + position: { x: 250, y: 200 }, + height: 100, + width: 100, parentId: '5', expandParent: true, style: { ...nodeStyle }, diff --git a/examples/react/src/examples/Subflow/DebugNode.tsx b/examples/react/src/examples/Subflow/DebugNode.tsx index 6a8a2d26..547da524 100644 --- a/examples/react/src/examples/Subflow/DebugNode.tsx +++ b/examples/react/src/examples/Subflow/DebugNode.tsx @@ -1,6 +1,6 @@ import React, { memo, FC, CSSProperties } from 'react'; -import { Handle, NodeProps, Position } from '@xyflow/react'; +import { Handle, NodeProps, Position, useInternalNode } from '@xyflow/react'; const infoStyle: CSSProperties = { fontSize: 11 }; const idStyle: CSSProperties = { @@ -12,6 +12,8 @@ const idStyle: CSSProperties = { }; const DebugNode: FC = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => { + const node = useInternalNode(id)!; + return ( <> @@ -19,6 +21,9 @@ const DebugNode: FC = ({ zIndex, positionAbsoluteX, positionAbsoluteY
x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
+
+ x:{Math.round(node.position.x)} y:{Math.round(node.position.y)} +
); diff --git a/examples/react/src/examples/Subflow/index.tsx b/examples/react/src/examples/Subflow/index.tsx index 7f1b3f4b..74654870 100644 --- a/examples/react/src/examples/Subflow/index.tsx +++ b/examples/react/src/examples/Subflow/index.tsx @@ -14,6 +14,8 @@ import { Background, Panel, NodeOrigin, + useUpdateNodeInternals, + ReactFlowProvider, } from '@xyflow/react'; import DebugNode from './DebugNode'; @@ -104,7 +106,7 @@ const initialNodes: Node[] = [ { id: '5b', data: { label: 'Node 5b' }, - position: { x: 225, y: 50 }, + position: { x: 200, y: 200 }, className: 'light', parentId: '5', expandParent: true, @@ -151,6 +153,7 @@ const nodeTypes = { const Subflow = () => { const [rfInstance, setRfInstance] = useState(null); + const updateNodeInternals = useUpdateNodeInternals(); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -216,6 +219,7 @@ const Subflow = () => { onlyRenderVisibleElements={false} nodeTypes={nodeTypes} fitView + nodeOrigin={[0, 0]} > @@ -228,9 +232,14 @@ const Subflow = () => { + ); }; -export default Subflow; +export default () => ( + + + +); diff --git a/packages/react/src/additional-components/MiniMap/MiniMap.tsx b/packages/react/src/additional-components/MiniMap/MiniMap.tsx index 0a05890d..bd7d6634 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMap.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMap.tsx @@ -25,12 +25,8 @@ const selector = (s: ReactFlowState) => { return { viewBB, - boundingRect: - s.nodeLookup.size > 0 - ? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { nodeOrigin: s.nodeOrigin }), viewBB) - : viewBB, + boundingRect: s.nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup), viewBB) : viewBB, rfId: s.rfId, - nodeOrigin: s.nodeOrigin, panZoom: s.panZoom, translateExtent: s.translateExtent, flowWidth: s.width, diff --git a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx index da6d3745..5bb7e939 100644 --- a/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx +++ b/packages/react/src/additional-components/MiniMap/MiniMapNodes.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ComponentType, memo } from 'react'; -import { NodeOrigin, getNodeDimensions, getNodePositionWithOrigin, nodeHasDimensions } from '@xyflow/system'; +import { getNodeDimensions, nodeHasDimensions } from '@xyflow/system'; import { shallow } from 'zustand/shallow'; import { useStore } from '../../hooks/useStore'; @@ -11,7 +11,6 @@ import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMa declare const window: any; -const selector = (s: ReactFlowState) => s.nodeOrigin; const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id); const getAttrFunction = (func: any): GetMiniMapNodeAttribute => func instanceof Function ? func : () => func; @@ -28,7 +27,6 @@ function MiniMapNodes({ onClick, }: MiniMapNodesProps) { const nodeIds = useStore(selectorNodeIds, shallow); - const nodeOrigin = useStore(selector); const nodeColorFunc = getAttrFunction(nodeColor); const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor); const nodeClassNameFunc = getAttrFunction(nodeClassName); @@ -46,7 +44,6 @@ function MiniMapNodes({ key={nodeId} id={nodeId} - nodeOrigin={nodeOrigin} nodeColorFunc={nodeColorFunc} nodeStrokeColorFunc={nodeStrokeColorFunc} nodeClassNameFunc={nodeClassNameFunc} @@ -63,7 +60,6 @@ function MiniMapNodes({ function NodeComponentWrapperInner({ id, - nodeOrigin, nodeColorFunc, nodeStrokeColorFunc, nodeClassNameFunc, @@ -74,7 +70,6 @@ function NodeComponentWrapperInner({ onClick, }: { id: string; - nodeOrigin: NodeOrigin; nodeColorFunc: GetMiniMapNodeAttribute; nodeStrokeColorFunc: GetMiniMapNodeAttribute; nodeClassNameFunc: GetMiniMapNodeAttribute; @@ -86,7 +81,7 @@ function NodeComponentWrapperInner({ }) { const { node, x, y } = useStore((s) => { const node = s.nodeLookup.get(id) as InternalNode; - const { x, y } = getNodePositionWithOrigin(node, nodeOrigin).positionAbsolute; + const { x, y } = node.internals.positionAbsolute; return { node, diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index 79746f84..63128c09 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -67,26 +67,30 @@ function ResizeControl({ }, onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { const { triggerNodeChanges, nodeLookup, parentLookup, nodeOrigin } = store.getState(); - const changes: NodeChange[] = []; const nextPosition = { x: change.x, y: change.y }; - const node = nodeLookup.get(id); + if (node && node.expandParent && node.parentId) { + const origin = node.origin ?? nodeOrigin; + const width = change.width ?? node.measured.width!; + const height = change.height ?? node.measured.height!; + const child: ParentExpandChild = { id: node.id, parentId: node.parentId, rect: { - width: change.width ?? node.measured.width!, - height: change.height ?? node.measured.height!, + width, + height, ...evaluateAbsolutePosition( { x: change.x ?? node.position.x, y: change.y ?? node.position.y, }, + { width, height }, node.parentId, nodeLookup, - node.origin ?? nodeOrigin + origin ), }, }; @@ -94,9 +98,10 @@ function ResizeControl({ const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin); changes.push(...parentExpandChanges); - // when the parent was expanded by the child node, its position will be clamped at 0,0 - nextPosition.x = change.x ? Math.max(0, change.x) : undefined; - nextPosition.y = change.y ? Math.max(0, change.y) : undefined; + // when the parent was expanded by the child node, its position will be clamped at + // 0,0 when node origin is 0,0 and to width, height if it's 1,1 + nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined; + nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined; } if (nextPosition.x !== undefined && nextPosition.y !== undefined) { diff --git a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx index f14f822e..b4431157 100644 --- a/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx +++ b/packages/react/src/additional-components/NodeToolbar/NodeToolbar.tsx @@ -1,7 +1,7 @@ import { useCallback, CSSProperties } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; -import { Rect, Position, getNodeToolbarTransform, getNodesBounds } from '@xyflow/system'; +import { Position, getNodeToolbarTransform, getInternalNodesBounds, NodeLookup } from '@xyflow/system'; import { InternalNode, ReactFlowState } from '../../types'; import { useStore } from '../../hooks/useStore'; @@ -17,21 +17,24 @@ const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) => a?.selected !== b?.selected || a?.internals.z !== b?.internals.z; -const nodesEqualityFn = (a: InternalNode[], b: InternalNode[]) => { - if (a.length !== b.length) { +const nodesEqualityFn = (a: NodeLookup, b: NodeLookup) => { + if (a.size !== b.size) { return false; } - return !a.some((node, i) => nodeEqualityFn(node, b[i])); + for (const [key, node] of a) { + if (nodeEqualityFn(node, b.get(key))) { + return false; + } + } + + return true; }; const storeSelector = (state: ReactFlowState) => ({ - viewport: { - x: state.transform[0], - y: state.transform[1], - zoom: state.transform[2], - }, - nodeOrigin: state.nodeOrigin, + x: state.transform[0], + y: state.transform[1], + zoom: state.transform[2], selectedNodesCount: state.nodes.filter((node) => node.selected).length, }); @@ -49,36 +52,41 @@ export function NodeToolbar({ const contextNodeId = useNodeId(); const nodesSelector = useCallback( - (state: ReactFlowState): InternalNode[] => { + (state: ReactFlowState): NodeLookup => { const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || '']; - - return nodeIds.reduce((acc, id) => { + const internalNodes = nodeIds.reduce((res, id) => { const node = state.nodeLookup.get(id); if (node) { - acc.push(node); + res.set(node.id, node); } - return acc; - }, []); + + return res; + }, new Map()); + + return internalNodes; }, [nodeId, contextNodeId] ); const nodes = useStore(nodesSelector, nodesEqualityFn); - const { viewport, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow); + const { x, y, zoom, selectedNodesCount } = useStore(storeSelector, shallow); // if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected const isActive = - typeof isVisible === 'boolean' ? isVisible : nodes.length === 1 && nodes[0].selected && selectedNodesCount === 1; + typeof isVisible === 'boolean' + ? isVisible + : nodes.size === 1 && nodes.values().next().value.selected && selectedNodesCount === 1; - if (!isActive || !nodes.length) { + if (!isActive || !nodes.size) { return null; } - const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin }); - const zIndex: number = Math.max(...nodes.map((node) => node.internals.z + 1)); + const nodeRect = getInternalNodesBounds(nodes); + const nodesArray = Array.from(nodes.values()); + const zIndex = Math.max(...nodesArray.map((node) => node.internals.z + 1)); const wrapperStyle: CSSProperties = { position: 'absolute', - transform: getNodeToolbarTransform(nodeRect, viewport, position, offset, align), + transform: getNodeToolbarTransform(nodeRect, { x, y, zoom }, position, offset, align), zIndex, ...style, }; @@ -89,7 +97,8 @@ export function NodeToolbar({ style={wrapperStyle} className={cc(['react-flow__node-toolbar', className])} {...rest} - data-id={nodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()} + // @todo: check if we could only do this for non-prod envs + data-id={nodesArray.reduce((acc, node) => `${acc}${node.id} `, '').trim()} > {children} diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 279cda82..655233c9 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -6,7 +6,6 @@ import { elementSelectionKeys, errorMessages, getNodeDimensions, - getPositionWithOrigin, isInputDOMNode, nodeHasDimensions, } from '@xyflow/system'; @@ -40,7 +39,6 @@ export function NodeWrapper({ rfId, nodeTypes, nodeExtent, - nodeOrigin, onError, }: NodeWrapperProps) { const { node, internals, isParent } = useStore((s) => { @@ -87,15 +85,11 @@ export function NodeWrapper({ const nodeDimensions = getNodeDimensions(node); const inlineDimensions = getNodeInlineStyleDimensions(node); + // TODO: clamping should happen earlier const clampedPosition = nodeExtent ? clampPosition(internals.positionAbsolute, nodeExtent) : internals.positionAbsolute; - const positionWithOrigin = getPositionWithOrigin({ - ...clampedPosition, - ...nodeDimensions, - origin: node.origin || nodeOrigin, - }); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const onMouseEnterHandler = onMouseEnter @@ -181,7 +175,7 @@ export function NodeWrapper({ ref={nodeRef} style={{ zIndex: internals.z, - transform: `translate(${positionWithOrigin.x}px,${positionWithOrigin.y}px)`, + transform: `translate(${clampedPosition.x}px,${clampedPosition.y}px)`, pointerEvents: hasPointerEvents ? 'all' : 'none', visibility: hasDimensions ? 'visible' : 'hidden', ...node.style, diff --git a/packages/react/src/components/NodesSelection/index.tsx b/packages/react/src/components/NodesSelection/index.tsx index fda3a8c9..e2bcd238 100644 --- a/packages/react/src/components/NodesSelection/index.tsx +++ b/packages/react/src/components/NodesSelection/index.tsx @@ -21,7 +21,6 @@ export type NodesSelectionProps = { const selector = (s: ReactFlowState) => { const { width, height, x, y } = getInternalNodesBounds(s.nodeLookup, { - nodeOrigin: s.nodeOrigin, filter: (node) => !!node.selected, }); diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index f04ebebd..4843afeb 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -4,6 +4,7 @@ import { Provider } from '../../contexts/StoreContext'; import { createStore } from '../../store'; import { BatchProvider } from '../BatchProvider'; import type { Node, Edge } from '../../types'; +import { NodeOrigin } from '@xyflow/system'; export type ReactFlowProviderProps = { initialNodes?: Node[]; @@ -13,6 +14,7 @@ export type ReactFlowProviderProps = { initialWidth?: number; initialHeight?: number; fitView?: boolean; + nodeOrigin?: NodeOrigin; children: ReactNode; }; @@ -24,6 +26,7 @@ export function ReactFlowProvider({ initialWidth: width, initialHeight: height, fitView, + nodeOrigin, children, }: ReactFlowProviderProps) { const [store] = useState(() => @@ -35,6 +38,7 @@ export function ReactFlowProvider({ width, height, fitView, + nodeOrigin, }) ); diff --git a/packages/react/src/container/FlowRenderer/index.tsx b/packages/react/src/container/FlowRenderer/index.tsx index 30d17979..f3e0a23d 100644 --- a/packages/react/src/container/FlowRenderer/index.tsx +++ b/packages/react/src/container/FlowRenderer/index.tsx @@ -22,7 +22,6 @@ export type FlowRendererProps = Omit< | 'selectNodesOnDrag' | 'defaultMarkerColor' | 'rfId' - | 'nodeOrigin' > & { isControlledViewport: boolean; children: ReactNode; diff --git a/packages/react/src/container/GraphView/index.tsx b/packages/react/src/container/GraphView/index.tsx index 8050ca20..d74a0fdb 100644 --- a/packages/react/src/container/GraphView/index.tsx +++ b/packages/react/src/container/GraphView/index.tsx @@ -32,7 +32,6 @@ export type GraphViewProps > & { rfId: string; @@ -97,7 +96,6 @@ function GraphViewComponent diff --git a/packages/react/src/container/NodeRenderer/index.tsx b/packages/react/src/container/NodeRenderer/index.tsx index 39b20323..2b178ae5 100644 --- a/packages/react/src/container/NodeRenderer/index.tsx +++ b/packages/react/src/container/NodeRenderer/index.tsx @@ -22,7 +22,6 @@ export type NodeRendererProps = Pick< | 'noDragClassName' | 'rfId' | 'disableKeyboardA11y' - | 'nodeOrigin' | 'nodeExtent' | 'nodeTypes' >; @@ -72,7 +71,6 @@ function NodeRendererComponent(props: NodeRendererProps { - const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = + const { userSelectionRect, edgeLookup, transform, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = store.getState(); if (!containerBounds.current || !userSelectionRect) { @@ -181,8 +181,7 @@ export function Pane({ nextUserSelectRect, transform, selectionMode === SelectionMode.Partial, - true, - nodeOrigin + true ); const selectedEdgeIds = new Set(); diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx index af9ad4a4..ce1b5a74 100644 --- a/packages/react/src/container/ReactFlow/Wrapper.tsx +++ b/packages/react/src/container/ReactFlow/Wrapper.tsx @@ -3,6 +3,7 @@ import { useContext, type ReactNode } from 'react'; import StoreContext from '../../contexts/StoreContext'; import { ReactFlowProvider } from '../../components/ReactFlowProvider'; import type { Node, Edge } from '../../types'; +import { NodeOrigin } from '@xyflow/system'; export function Wrapper({ children, @@ -13,6 +14,7 @@ export function Wrapper({ width, height, fitView, + nodeOrigin, }: { children: ReactNode; nodes?: Node[]; @@ -22,6 +24,7 @@ export function Wrapper({ width?: number; height?: number; fitView?: boolean; + nodeOrigin?: NodeOrigin; }) { const isWrapped = useContext(StoreContext); @@ -40,6 +43,7 @@ export function Wrapper({ initialWidth={width} initialHeight={height} fitView={fitView} + nodeOrigin={nodeOrigin} > {children} diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index 64ac7202..c301d9d4 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -157,7 +157,7 @@ function ReactFlow( data-testid="rf__wrapper" id={id} > - + onInit={onInit} onNodeClick={onNodeClick} @@ -217,7 +217,6 @@ function ReactFlow( noPanClassName={noPanClassName} rfId={rfId} disableKeyboardA11y={disableKeyboardA11y} - nodeOrigin={nodeOrigin} nodeExtent={nodeExtent} viewport={viewport} onViewportChange={onViewportChange} diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 6766e775..faf9d31f 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -50,7 +50,7 @@ export function useReactFlow(node) ? node : nodeLookup.get(node.id)!; const position = nodeToUse.parentId - ? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.parentId, nodeLookup, nodeOrigin) + ? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.measured, nodeToUse.parentId, nodeLookup, nodeOrigin) : nodeToUse.position; const nodeWithPosition = { diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index ef4f6f38..245fafd7 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -45,22 +45,23 @@ const useViewportHelper = (): ViewportHelperFunctions => { return { x, y, zoom }; }, fitView: (options) => { - const { nodeLookup, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState(); + const { nodeLookup, width, height, minZoom, maxZoom, panZoom } = store.getState(); - return panZoom - ? fitView( - { - nodeLookup, - width, - height, - nodeOrigin, - minZoom, - maxZoom, - panZoom, - }, - options - ) - : false; + if (!panZoom) { + return false; + } + + return fitView( + { + nodeLookup, + width, + height, + minZoom, + maxZoom, + panZoom, + }, + options + ); }, setCenter: (x, y, options) => { const { width, height, maxZoom, panZoom } = store.getState(); diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 29bf5ea5..5ccd7070 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -12,6 +12,7 @@ import { EdgeSelectionChange, NodeSelectionChange, ParentExpandChild, + NodeOrigin, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -26,6 +27,7 @@ const createStore = ({ width, height, fitView, + nodeOrigin, }: { nodes?: Node[]; edges?: Edge[]; @@ -34,10 +36,11 @@ const createStore = ({ width?: number; height?: number; fitView?: boolean; + nodeOrigin?: NodeOrigin; }) => createWithEqualityFn( (set, get) => ({ - ...getInitialState({ nodes, edges, width, height, fitView, defaultNodes, defaultEdges }), + ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); // setNodes() is called exclusively in response to user actions: @@ -98,7 +101,7 @@ const createStore = ({ return; } - updateAbsolutePositions(nodeLookup, { nodeOrigin }); + updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin }); // we call fitView once initially after all dimensions are set let nextFitViewDone = fitViewDone; @@ -155,8 +158,8 @@ const createStore = ({ } if (parentExpandChildren.length > 0) { - const { nodeLookup, parentLookup } = get(); - const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup); + const { nodeLookup, parentLookup, nodeOrigin } = get(); + const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin); changes.push(...parentExpandChanges); } @@ -288,7 +291,7 @@ const createStore = ({ return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); }, fitView: (options?: FitViewOptions): boolean => { - const { panZoom, width, height, minZoom, maxZoom, nodeOrigin, nodeLookup } = get(); + const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); if (!panZoom) { return false; @@ -302,7 +305,6 @@ const createStore = ({ panZoom, minZoom, maxZoom, - nodeOrigin, }, options ); diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 1a29e5df..495afd3d 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -7,6 +7,7 @@ import { updateConnectionLookup, devWarn, getInternalNodesBounds, + NodeOrigin, } from '@xyflow/system'; import type { Edge, InternalNode, Node, ReactFlowStore } from '../types'; @@ -19,6 +20,7 @@ const getInitialState = ({ width, height, fitView, + nodeOrigin, }: { nodes?: Node[]; edges?: Edge[]; @@ -27,6 +29,7 @@ const getInitialState = ({ width?: number; height?: number; fitView?: boolean; + nodeOrigin?: NodeOrigin; } = {}): ReactFlowStore => { const nodeLookup = new Map(); const parentLookup = new Map(); @@ -34,21 +37,21 @@ const getInitialState = ({ const edgeLookup = new Map(); const storeEdges = defaultEdges ?? edges ?? []; const storeNodes = defaultNodes ?? nodes ?? []; + const storeNodeOrigin = nodeOrigin ?? [0, 0]; updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); adoptUserNodes(storeNodes, nodeLookup, parentLookup, { - nodeOrigin: [0, 0], + nodeOrigin: storeNodeOrigin, elevateNodesOnSelect: false, }); let transform: Transform = [0, 0, 1]; if (fitView && width && height) { - // @todo users nodeOrigin should be used here const bounds = getInternalNodesBounds(nodeLookup, { - nodeOrigin: [0, 0], filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight)), }); + const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); transform = [x, y, zoom]; } @@ -80,7 +83,7 @@ const getInitialState = ({ domNode: null, paneDragging: false, noPanClassName: 'nopan', - nodeOrigin: [0, 0], + nodeOrigin: storeNodeOrigin, nodeDragThreshold: 1, snapGrid: [15, 15], diff --git a/packages/react/src/types/nodes.ts b/packages/react/src/types/nodes.ts index a01a894a..71e03036 100644 --- a/packages/react/src/types/nodes.ts +++ b/packages/react/src/types/nodes.ts @@ -1,12 +1,5 @@ import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; -import type { - CoordinateExtent, - NodeBase, - NodeOrigin, - OnError, - NodeProps as NodePropsBase, - InternalNodeBase, -} from '@xyflow/system'; +import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system'; import { NodeTypes } from './general'; @@ -59,7 +52,6 @@ export type NodeWrapperProps = { disableKeyboardA11y: boolean; nodeTypes?: NodeTypes; nodeExtent?: CoordinateExtent; - nodeOrigin: NodeOrigin; onError?: OnError; }; diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 8ef580b2..ab99ad45 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -26,6 +26,7 @@ import { type NodeLookup, type NodeChange, type EdgeChange, + type ParentLookup, } from '@xyflow/system'; import type { @@ -53,7 +54,7 @@ export type ReactFlowStore>; - parentLookup: Map[]>; + parentLookup: ParentLookup>; edges: Edge[]; edgeLookup: EdgeLookup; connectionLookup: ConnectionLookup; diff --git a/packages/svelte/src/lib/actions/drag/index.ts b/packages/svelte/src/lib/actions/drag/index.ts index 1405ccd9..f5649a29 100644 --- a/packages/svelte/src/lib/actions/drag/index.ts +++ b/packages/svelte/src/lib/actions/drag/index.ts @@ -34,7 +34,7 @@ export default function drag(domNode: Element, params: UseDragParams) { nodeExtent: get(store.nodeExtent), snapGrid: snapGrid ? snapGrid : [0, 0], snapToGrid: !!snapGrid, - nodeOrigin: [0, 0], + nodeOrigin: get(store.nodeOrigin), multiSelectionActive: get(store.multiselectionKeyPressed), domNode: get(store.domNode), transform: [vp.x, vp.y, vp.zoom], diff --git a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte index 6fe6b648..3c77ddef 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte +++ b/packages/svelte/src/lib/components/NodeWrapper/NodeWrapper.svelte @@ -31,8 +31,6 @@ export let isParent: $$Props['isParent'] = false; export let positionX: $$Props['positionX']; export let positionY: $$Props['positionY']; - export let positionOriginX: $$Props['positionOriginX']; - export let positionOriginY: $$Props['positionOriginY']; export let sourcePosition: $$Props['sourcePosition'] = undefined; export let targetPosition: $$Props['targetPosition'] = undefined; export let zIndex: $$Props['zIndex']; @@ -181,7 +179,7 @@ class:nopan={draggable} class:parent={isParent} style:z-index={zIndex} - style:transform="translate({positionOriginX}px, {positionOriginY}px)" + style:transform="translate({positionX}px, {positionY}px)" style:visibility={initialized ? 'visible' : 'hidden'} style="{style ?? ''};{inlineStyleDimensions.width}{inlineStyleDimensions.height}" on:click={onSelectNodeHandler} diff --git a/packages/svelte/src/lib/components/NodeWrapper/types.ts b/packages/svelte/src/lib/components/NodeWrapper/types.ts index c0f5ca79..11baf5b9 100644 --- a/packages/svelte/src/lib/components/NodeWrapper/types.ts +++ b/packages/svelte/src/lib/components/NodeWrapper/types.ts @@ -28,8 +28,6 @@ export type NodeWrapperProps = Pick< type: string; positionX: number; positionY: number; - positionOriginX: number; - positionOriginY: number; 'on:nodeclick'?: (event: MouseEvent) => void; resizeObserver?: ResizeObserver | null; isParent?: boolean; diff --git a/packages/svelte/src/lib/components/SvelteFlowProvider/SvelteFlowProvider.svelte b/packages/svelte/src/lib/components/SvelteFlowProvider/SvelteFlowProvider.svelte index b943ab0a..053373c0 100644 --- a/packages/svelte/src/lib/components/SvelteFlowProvider/SvelteFlowProvider.svelte +++ b/packages/svelte/src/lib/components/SvelteFlowProvider/SvelteFlowProvider.svelte @@ -11,12 +11,14 @@ export let initialWidth: $$Props['initialWidth'] = undefined; export let initialHeight: $$Props['initialHeight'] = undefined; export let fitView: $$Props['fitView'] = undefined; + export let nodeOrigin: $$Props['nodeOrigin'] = undefined; const store = createStore({ nodes: initialNodes, edges: initialEdges, width: initialWidth, height: initialHeight, + nodeOrigin, fitView }); diff --git a/packages/svelte/src/lib/components/SvelteFlowProvider/types.ts b/packages/svelte/src/lib/components/SvelteFlowProvider/types.ts index 769588b2..39fa5350 100644 --- a/packages/svelte/src/lib/components/SvelteFlowProvider/types.ts +++ b/packages/svelte/src/lib/components/SvelteFlowProvider/types.ts @@ -1,4 +1,5 @@ import type { Edge, Node } from '$lib/types'; +import type { NodeOrigin } from '@xyflow/system'; export type SvelteFlowProviderProps = { initialNodes?: Node[]; @@ -6,4 +7,5 @@ export type SvelteFlowProviderProps = { initialWidth?: number; initialHeight?: number; fitView?: boolean; + nodeOrigin?: NodeOrigin; }; diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index 4afc5637..b20329b3 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -1,6 +1,6 @@