From ee1ef205ebdc01d357465afacbda5988400f4daf Mon Sep 17 00:00:00 2001 From: peterkogo Date: Wed, 5 Mar 2025 13:45:28 +0100 Subject: [PATCH] Refactor fitView --- examples/react/src/examples/Basic/index.tsx | 16 ++- .../src/components/StoreUpdater/index.tsx | 4 +- packages/react/src/hooks/useReactFlow.ts | 14 +- packages/react/src/hooks/useViewportHelper.ts | 25 ---- packages/react/src/store/index.ts | 121 +++++------------- packages/react/src/store/initialState.ts | 6 +- packages/react/src/types/general.ts | 13 +- packages/react/src/types/instance.ts | 13 +- packages/react/src/types/store.ts | 7 +- packages/system/src/utils/graph.ts | 22 ++-- packages/system/src/utils/store.ts | 9 +- 11 files changed, 99 insertions(+), 151 deletions(-) diff --git a/examples/react/src/examples/Basic/index.tsx b/examples/react/src/examples/Basic/index.tsx index 62232a35..bb53b309 100644 --- a/examples/react/src/examples/Basic/index.tsx +++ b/examples/react/src/examples/Basic/index.tsx @@ -56,8 +56,18 @@ const initialEdges: Edge[] = [ const defaultEdgeOptions = {}; const BasicFlow = () => { - const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } = - useReactFlow(); + const { + addNodes, + setNodes, + getNodes, + setEdges, + getEdges, + deleteElements, + updateNodeData, + toObject, + setViewport, + fitView, + } = useReactFlow(); const updatePos = () => { setNodes((nodes) => @@ -104,6 +114,7 @@ const BasicFlow = () => { ]); setEdges([{ id: 'a-b', source: 'a', target: 'b' }]); + fitView(); }; const onUpdateNode = () => { @@ -117,6 +128,7 @@ const BasicFlow = () => { position: { x: Math.random() * 300, y: Math.random() * 300 }, className: 'light', }); + fitView(); }; return ( diff --git a/packages/react/src/components/StoreUpdater/index.tsx b/packages/react/src/components/StoreUpdater/index.tsx index e3a61ab2..ac9122ba 100644 --- a/packages/react/src/components/StoreUpdater/index.tsx +++ b/packages/react/src/components/StoreUpdater/index.tsx @@ -154,8 +154,8 @@ export function StoreUpdater !!s.panZoom; @@ -271,6 +279,10 @@ export function useReactFlow | undefined) => { + store.setState({ fitViewQueued: true, fitViewOptions: options }); + batchContext.nodeQueue.push((nodes) => [...nodes]); + }, }; }, []); diff --git a/packages/react/src/hooks/useViewportHelper.ts b/packages/react/src/hooks/useViewportHelper.ts index 9a773d14..a88bd3fc 100644 --- a/packages/react/src/hooks/useViewportHelper.ts +++ b/packages/react/src/hooks/useViewportHelper.ts @@ -2,11 +2,8 @@ import { useMemo } from 'react'; import { pointToRendererPoint, getViewportForBounds, - getFitViewNodes, - fitView, type XYPosition, rendererPointToPoint, - getDimensions, SnapGrid, } from '@xyflow/system'; @@ -65,28 +62,6 @@ const useViewportHelper = (): ViewportHelperFunctions => { const [x, y, zoom] = store.getState().transform; return { x, y, zoom }; }, - fitView: (options) => { - const { nodeLookup, minZoom, maxZoom, panZoom, domNode } = store.getState(); - - if (!panZoom || !domNode) { - return Promise.resolve(false); - } - - const fitViewNodes = getFitViewNodes(nodeLookup, options); - const { width, height } = getDimensions(domNode); - - return fitView( - { - nodes: fitViewNodes, - width, - height, - minZoom, - maxZoom, - panZoom, - }, - options - ); - }, setCenter: async (x, y, options) => { const { width, height, maxZoom, panZoom } = store.getState(); const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index bb05ae22..64d1b405 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -1,7 +1,5 @@ import { createWithEqualityFn } from 'zustand/traditional'; import { - getFitViewNodes, - fitView as fitViewSystem, adoptUserNodes, updateAbsolutePositions, panBy as panBySystem, @@ -15,11 +13,12 @@ import { initialConnection, NodeOrigin, CoordinateExtent, + fitViewport, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import getInitialState from './initialState'; -import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions } from '../types'; +import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams } from '../types'; const createStore = ({ nodes, @@ -46,7 +45,7 @@ const createStore = ({ (set, get) => ({ ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { - const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); + const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, fitViewQueued, panZoom } = get(); /* * setNodes() is called exclusively in response to user actions: * - either when the `` prop is updated in the controlled ReactFlow setup, @@ -55,14 +54,35 @@ const createStore = ({ * When this happens, we take the note objects passed by the user and extend them with fields * relevant for internal React Flow operations. */ - adoptUserNodes(nodes, nodeLookup, parentLookup, { + + const nodesInitialized = adoptUserNodes(nodes, nodeLookup, parentLookup, { nodeOrigin, nodeExtent, elevateNodesOnSelect, checkEquality: true, }); - set({ nodes }); + let viewportFitted = false; + if (fitViewQueued && nodesInitialized && panZoom) { + const { fitViewOptions, width, height, minZoom, maxZoom } = get(); + viewportFitted = fitViewport( + { + nodes: nodeLookup, + width, + height, + panZoom, + minZoom, + maxZoom, + }, + fitViewOptions + ); + } + + if (viewportFitted) { + set({ nodes, fitViewQueued: false, fitViewOptions: undefined }); + } else { + set({ nodes }); + } }, setEdges: (edges: Edge[]) => { const { connectionLookup, edgeLookup } = get(); @@ -88,20 +108,8 @@ const createStore = ({ * changes its dimensions, this function is called to measure the * new dimensions and update the nodes. */ - updateNodeInternals: (updates, params = { triggerFitView: true }) => { - const { - triggerNodeChanges, - nodeLookup, - parentLookup, - fitViewOnInit, - fitViewDone, - fitViewOnInitOptions, - domNode, - nodeOrigin, - nodeExtent, - debug, - fitViewSync, - } = get(); + updateNodeInternals: (updates) => { + const { triggerNodeChanges, nodeLookup, parentLookup, domNode, nodeOrigin, nodeExtent, debug } = get(); const { changes, updatedInternals } = updateNodeInternalsSystem( updates, @@ -118,29 +126,8 @@ const createStore = ({ updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent }); - if (params.triggerFitView) { - // we call fitView once initially after all dimensions are set - let nextFitViewDone = fitViewDone; - - if (!fitViewDone && fitViewOnInit) { - nextFitViewDone = fitViewSync({ - ...fitViewOnInitOptions, - nodes: fitViewOnInitOptions?.nodes, - }); - } - - /* - * here we are cirmumventing the onNodesChange handler - * in order to be able to display nodes even if the user - * has not provided an onNodesChange handler. - * Nodes are only rendered if they have a width and height - * attribute which they get from this handler. - */ - set({ fitViewDone: nextFitViewDone }); - } else { - // we always want to trigger useStore calls whenever updateNodeInternals is called - set({}); - } + // we always want to trigger useStore calls whenever updateNodeInternals is called + set({}); if (changes?.length > 0) { if (debug) { @@ -332,54 +319,6 @@ const createStore = ({ return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); }, - fitView: (options?: FitViewOptions): Promise => { - const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); - - if (!panZoom) { - return Promise.resolve(false); - } - - const fitViewNodes = getFitViewNodes(nodeLookup, options); - - return fitViewSystem( - { - nodes: fitViewNodes, - width, - height, - panZoom, - minZoom, - maxZoom, - }, - options - ); - }, - /* - * we can't call an asnychronous function in updateNodeInternals - * for that we created this sync version of fitView - */ - fitViewSync: (options?: FitViewOptions): boolean => { - const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get(); - - if (!panZoom) { - return false; - } - - const fitViewNodes = getFitViewNodes(nodeLookup, options); - - fitViewSystem( - { - nodes: fitViewNodes, - width, - height, - panZoom, - minZoom, - maxZoom, - }, - options - ); - - return fitViewNodes.size > 0; - }, cancelConnection: () => { set({ connection: { ...initialConnection }, diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index af52f2a2..9005c99f 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -104,13 +104,13 @@ const getInitialState = ({ elementsSelectable: true, elevateNodesOnSelect: true, elevateEdgesOnSelect: false, - fitViewOnInit: false, - fitViewDone: false, - fitViewOnInitOptions: undefined, selectNodesOnDrag: true, multiSelectionActive: false, + fitViewQueued: false, + fitViewOptions: undefined, + connection: { ...initialConnection }, connectionClickStartHandle: null, connectOnClick: true, diff --git a/packages/react/src/types/general.ts b/packages/react/src/types/general.ts index 8b78e77b..45dc93c1 100644 --- a/packages/react/src/types/general.ts +++ b/packages/react/src/types/general.ts @@ -109,7 +109,7 @@ export type FitViewParams = FitViewParamsBase = FitViewOptionsBase; -export type FitView = (fitViewOptions?: FitViewOptions) => Promise; +export type FitView = (fitViewOptions?: FitViewOptions) => void; export type OnInit = ( reactFlowInstance: ReactFlowInstance ) => void; @@ -156,17 +156,6 @@ export type ViewportHelperFunctions = { * @returns Viewport */ getViewport: GetViewport; - /** - * Fits the view. - * - * @param options.padding - optional padding - * @param options.includeHiddenNodes - optional includeHiddenNodes - * @param options.minZoom - optional minZoom - * @param options.maxZoom - optional maxZoom - * @param options.duration - optional duration. If set, a transition will be applied - * @param options.nodes - optional nodes to fit the view to - */ - fitView: FitView; /** * Sets the center of the view to the given position. * diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index fce67ec5..7aa615f5 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-namespace */ import type { HandleConnection, HandleType, NodeConnection, Rect, Viewport } from '@xyflow/system'; -import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.'; +import type { Node, Edge, ViewportHelperFunctions, InternalNode, FitView } from '.'; export type ReactFlowJsonObject = { nodes: NodeType[]; @@ -217,6 +217,17 @@ export type GeneralHelpers NodeConnection[]; + // /** + // * Fits the view. + // * + // * @param options.padding - optional padding + // * @param options.includeHiddenNodes - optional includeHiddenNodes + // * @param options.minZoom - optional minZoom + // * @param options.maxZoom - optional maxZoom + // * @param options.duration - optional duration. If set, a transition will be applied + // * @param options.nodes - optional nodes to fit the view to + // */ + fitView: FitView; }; /** * The `ReactFlowInstance` provides a collection of methods to query and manipulate diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index e0ec76fa..10814ecf 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -119,9 +119,8 @@ export type ReactFlowStore; onEdgesDelete?: OnEdgesDelete; @@ -168,8 +167,6 @@ export type ReactFlowActions = { triggerNodeChanges: (changes: NodeChange[]) => void; triggerEdgeChanges: (changes: EdgeChange[]) => void; panBy: PanBy; - fitView: (options?: FitViewOptions) => Promise; - fitViewSync: (options?: FitViewOptions) => boolean; setPaneClickDistance: (distance: number) => void; }; diff --git a/packages/system/src/utils/graph.ts b/packages/system/src/utils/graph.ts index 9720e8c5..84738faf 100644 --- a/packages/system/src/utils/graph.ts +++ b/packages/system/src/utils/graph.ts @@ -333,10 +333,10 @@ export const getConnectedEdges = nodeIds.has(edge.source) || nodeIds.has(edge.target)); }; -export function getFitViewNodes< +function getFitViewNodes< Params extends NodeLookup>, Options extends FitViewOptionsBase ->(nodeLookup: Params, options?: Pick) { +>(nodeLookup: Params, options?: Options) { const fitViewNodes: NodeLookup = new Map(); const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null; @@ -351,15 +351,21 @@ export function getFitViewNodes< return fitViewNodes; } -export async function fitView, Options extends FitViewOptionsBase>( +export function fitViewport, Options extends FitViewOptionsBase>( { nodes, width, height, panZoom, minZoom, maxZoom }: Params, options?: Omit -): Promise { +): boolean { if (nodes.size === 0) { - return Promise.resolve(false); + return true; } - const bounds = getInternalNodesBounds(nodes); + if (!panZoom) { + return false; + } + + const nodesToFit = getFitViewNodes(nodes, options); + + const bounds = getInternalNodesBounds(nodesToFit); const viewport = getViewportForBounds( bounds, @@ -370,9 +376,9 @@ export async function fitView, Option options?.padding ?? 0.1 ); - await panZoom.setViewport(viewport, { duration: options?.duration }); + panZoom.setViewport(viewport, { duration: options?.duration }); - return Promise.resolve(true); + return true; } /** diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index a9d13316..03b4249c 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -85,9 +85,10 @@ export function adoptUserNodes( nodeLookup: NodeLookup>, parentLookup: ParentLookup>, options?: UpdateNodesOptions -) { +): boolean { const _options = mergeObjects(adoptUserNodesDefaultOptions, options); + let nodesInitialized = true; const tmpLookup = new Map(nodeLookup); const selectedNodeZ: number = _options?.elevateNodesOnSelect ? 1000 : 0; @@ -123,10 +124,16 @@ export function adoptUserNodes( nodeLookup.set(userNode.id, internalNode); } + if (!internalNode.measured || !internalNode.measured.width || !internalNode.measured.height) { + nodesInitialized = false; + } + if (userNode.parentId) { updateChildNode(internalNode, nodeLookup, parentLookup, options); } } + + return nodesInitialized; } function updateParentLookup(