diff --git a/example/src/Basic/index.js b/example/src/Basic/index.js index 2b38a838..3395d29b 100644 --- a/example/src/Basic/index.js +++ b/example/src/Basic/index.js @@ -41,6 +41,8 @@ const BasicFlow = () => { const logToObject = () => console.log(rfInstance.toObject()); + const resetTransform = () => rfInstance.setTransform({ x: 0, y: 0, zoom: 1 }); + return ( {
+ diff --git a/src/additional-components/Controls/index.tsx b/src/additional-components/Controls/index.tsx index 983701e9..ca11bfa3 100644 --- a/src/additional-components/Controls/index.tsx +++ b/src/additional-components/Controls/index.tsx @@ -1,4 +1,4 @@ -import React, { memo } from 'react'; +import React, { memo, useCallback } from 'react'; import cc from 'classcat'; import { useStoreState, useStoreActions } from '../../store/hooks'; @@ -39,56 +39,47 @@ const Controls = ({ const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable); const mapClasses = cc(['react-flow__controls', className]); + const onZoomInHandler = useCallback(() => { + zoomIn?.(); + onZoomIn?.(); + }, [zoomIn, onZoomIn]); + + const onZoomOutHandler = useCallback(() => { + zoomOut?.(); + onZoomOut?.(); + }, [zoomOut, onZoomOut]); + + const onFitViewHandler = useCallback(() => { + fitView?.(); + onFitView?.(); + }, [fitView, onFitView]); + + const onInteractiveChangeHandler = useCallback(() => { + setInteractive?.(!isInteractive); + onInteractiveChange?.(!isInteractive); + }, [isInteractive, setInteractive, onInteractiveChange]); + return (
{showZoom && ( <> -
{ - zoomIn(); - if (onZoomIn) { - onZoomIn(); - } - }} - > +
-
{ - zoomOut(); - if (onZoomOut) { - onZoomOut(); - } - }} - > +
)} {showFitView && ( -
{ - fitView({ padding: 0.1 }); - if (onFitView) { - onFitView(); - } - }} - > +
)} {showInteractive && (
{ - setInteractive(!isInteractive); - if (onInteractiveChange) { - onInteractiveChange(!isInteractive); - } - }} + onClick={onInteractiveChangeHandler} > {isInteractive ? : }
diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx index 72d8f2b8..44c209bd 100644 --- a/src/container/GraphView/index.tsx +++ b/src/container/GraphView/index.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, memo, CSSProperties, MouseEvent, WheelEvent } from 'react'; -import { useStoreState, useStoreActions, useStore } from '../../store/hooks'; +import { useStoreActions, useStore } from '../../store/hooks'; import FlowRenderer from '../FlowRenderer'; import NodeRenderer from '../NodeRenderer'; import EdgeRenderer from '../EdgeRenderer'; @@ -132,7 +132,6 @@ const GraphView = ({ onPaneContextMenu, }: GraphViewProps) => { const isInitialised = useRef(false); - const d3Zoom = useStoreState((state) => state.d3Zoom); const setOnConnect = useStoreActions((actions) => actions.setOnConnect); const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart); const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop); @@ -142,34 +141,32 @@ const GraphView = ({ const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable); const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable); const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable); - const setInitTransform = useStoreActions((actions) => actions.setInitTransform); const setMinZoom = useStoreActions((actions) => actions.setMinZoom); const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom); const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); const currentStore = useStore(); - const { zoomIn, zoomOut, zoomTo, fitView } = useZoomPanHelper(); + const { zoomIn, zoomOut, zoomTo, transform, fitView } = useZoomPanHelper(); useElementUpdater(elements); useEffect(() => { - if (!isInitialised.current && d3Zoom) { + if (!isInitialised.current && zoomIn && zoomOut && zoomTo && transform && fitView) { if (onLoad) { onLoad({ fitView: (params = { padding: 0.1 }) => fitView(params), zoomIn, zoomOut, zoomTo, + setTransform: transform, project: onLoadProject(currentStore), getElements: onLoadGetElements(currentStore), - setTransform: (transform: FlowTransform) => - setInitTransform({ x: transform.x, y: transform.y, k: transform.zoom }), toObject: onLoadToObject(currentStore), }); } isInitialised.current = true; } - }, [d3Zoom, onLoad]); + }, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView]); useEffect(() => { if (onConnect) { diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx index 4dcaba8b..3d2d94e3 100644 --- a/src/container/ZoomPane/index.tsx +++ b/src/container/ZoomPane/index.tsx @@ -123,7 +123,7 @@ const ZoomPane = ({ d3Zoom.on('zoom', null); } else { d3Zoom.on('zoom', (event: any) => { - updateTransform(event.transform); + updateTransform([event.transform.x, event.transform.y, event.transform.k]); if (onMove) { const flowTransform = eventToFlowTransform(event.transform); diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts index 239e3514..b2b5a0e7 100644 --- a/src/hooks/useZoomPanHelper.ts +++ b/src/hooks/useZoomPanHelper.ts @@ -1,67 +1,60 @@ -import { useCallback } from 'react'; +import { useMemo } from 'react'; import { zoomIdentity } from 'd3-zoom'; import { useStoreState, useStore } from '../store/hooks'; import { clamp } from '../utils'; import { getRectOfNodes } from '../utils/graph'; -import { FitViewParams } from '../types'; +import { FitViewParams, FlowTransform } from '../types'; + +const initialHelpers = { + zoomIn: null, + zoomOut: null, + zoomTo: null, + transform: null, + fitView: null, +}; export default () => { const store = useStore(); const d3Zoom = useStoreState((s) => s.d3Zoom); const d3Selection = useStoreState((s) => s.d3Selection); - const zoomIn = useCallback(() => { - if (d3Selection) { - d3Zoom?.scaleBy(d3Selection, 1.2); + const zoomPanHelperFunctions = useMemo(() => { + if (d3Selection && d3Zoom) { + return { + zoomIn: () => d3Zoom.scaleBy(d3Selection, 1.2), + zoomOut: () => d3Zoom.scaleBy(d3Selection, 1 / 1.2), + zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel), + transform: (transform: FlowTransform) => { + const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom); + + d3Zoom.transform(d3Selection, nextTransform); + }, + fitView: (options: FitViewParams = { padding: 0.1 }) => { + const { nodes, width, height, minZoom, maxZoom } = store.getState(); + + if (!nodes.length) { + return; + } + + const bounds = getRectOfNodes(nodes); + const xZoom = width / (bounds.width * (1 + options.padding)); + const yZoom = height / (bounds.height * (1 + options.padding)); + const zoom = Math.min(xZoom, yZoom); + const clampedZoom = clamp(zoom, minZoom, maxZoom); + const boundsCenterX = bounds.x + bounds.width / 2; + const boundsCenterY = bounds.y + bounds.height / 2; + const x = width / 2 - boundsCenterX * clampedZoom; + const y = height / 2 - boundsCenterY * clampedZoom; + const transform = zoomIdentity.translate(x, y).scale(clampedZoom); + + d3Zoom.transform(d3Selection, transform); + }, + }; } + + return initialHelpers; }, [d3Zoom, d3Selection]); - const zoomOut = useCallback(() => { - if (d3Selection) { - d3Zoom?.scaleBy(d3Selection, 1 / 1.2); - } - }, [d3Zoom, d3Selection]); - - const zoomTo = useCallback( - (zoomLevel: number) => { - if (d3Selection) { - d3Zoom?.scaleTo(d3Selection, zoomLevel); - } - }, - [d3Zoom, d3Selection] - ); - - const fitView = useCallback( - (options: FitViewParams) => { - const { padding = 0.1 } = options; - const { nodes, width, height, minZoom, maxZoom } = store.getState(); - - if (!d3Selection || !nodes.length) { - return; - } - - const bounds = getRectOfNodes(nodes); - const xZoom = width / (bounds.width * (1 + padding)); - const yZoom = height / (bounds.height * (1 + padding)); - const zoom = Math.min(xZoom, yZoom); - const clampedZoom = clamp(zoom, minZoom, maxZoom); - const boundsCenterX = bounds.x + bounds.width / 2; - const boundsCenterY = bounds.y + bounds.height / 2; - const x = width / 2 - boundsCenterX * clampedZoom; - const y = height / 2 - boundsCenterY * clampedZoom; - const k = clampedZoom; - const transform = zoomIdentity.translate(x, y).scale(k); - - d3Zoom?.transform(d3Selection, transform); - }, - [store, d3Zoom, d3Selection] - ); - - return { - zoomIn, - zoomOut, - zoomTo, - fitView, - }; + return zoomPanHelperFunctions; }; diff --git a/src/index.ts b/src/index.ts index 3fe2e30e..6ce7ecc2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ export { getSmoothStepPath } from './components/Edges/SmoothStepEdge'; export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils'; export { isNode, isEdge, removeElements, addEdge, getOutgoers, getIncomers, getConnectedEdges } from './utils/graph'; +export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'; export * from './additional-components'; export * from './store/hooks'; diff --git a/src/store/index.ts b/src/store/index.ts index 05e550cf..406ac647 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,12 +1,11 @@ import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy'; import isEqual from 'fast-deep-equal'; import { Selection as D3Selection, ZoomBehavior } from 'd3'; -import { zoomIdentity } from 'd3-zoom'; import { getDimensions } from '../utils'; +import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph'; import { getHandleBounds } from '../components/Nodes/utils'; -import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph'; import { ElementId, Elements, @@ -29,12 +28,6 @@ import { SnapGrid, } from '../types'; -type TransformXYK = { - x: number; - y: number; - k: number; -}; - type NodeDimensionUpdate = { id: ElementId; nodeElement: HTMLDivElement; @@ -110,9 +103,7 @@ export interface StoreModel { setSelectedElements: Action; addSelectedElements: Thunk; - updateTransform: Action; - - setInitTransform: Action; + updateTransform: Action; updateSize: Action; @@ -148,8 +139,8 @@ export const storeModel: StoreModel = { viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })), transform: [0, 0, 1], elements: [], - nodes: computed((state) => state.elements.filter((el) => isNode(el)) as Node[]), - edges: computed((state) => state.elements.filter((el) => isEdge(el)) as Edge[]), + nodes: computed((state) => state.elements.filter(isNode)), + edges: computed((state) => state.elements.filter(isEdge)), selectedElements: null, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, @@ -363,21 +354,9 @@ export const storeModel: StoreModel = { }), updateTransform: action((state, transform) => { - state.transform[0] = transform.x; - state.transform[1] = transform.y; - state.transform[2] = transform.k; - }), - - setInitTransform: action((state, transform) => { - state.transform[0] = transform.x; - state.transform[1] = transform.y; - state.transform[2] = transform.k; - - if (state.d3Selection) { - const updatedTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.k); - // we need to sync the d3 zoom transform with the updated transform - state.d3Selection.property('__zoom', updatedTransform); - } + state.transform[0] = transform[0]; + state.transform[1] = transform[1]; + state.transform[2] = transform[2]; }), updateSize: action((state, size) => {