{
- zoomIn();
- if (onZoomIn) {
- onZoomIn();
- }
- }}>
+
{
- zoomOut();
- if (onZoomOut) {
- onZoomOut();
- }
- }}>
+
{
+ zoomOut();
+ if (onZoomOut) {
+ onZoomOut();
+ }
+ }}
+ >
>
diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx
index eb68438d..3f9ce248 100644
--- a/src/container/GraphView/index.tsx
+++ b/src/container/GraphView/index.tsx
@@ -22,6 +22,7 @@ import {
OnConnectEndFunc,
TranslateExtent,
} from '../../types';
+import useZoomPanHelper from '../../hooks/useZoomPanHelper';
export interface GraphViewProps {
elements: Elements;
@@ -145,10 +146,8 @@ const GraphView = ({
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
- const fitView = useStoreActions((actions) => actions.fitView);
- const zoom = useStoreActions((actions) => actions.zoom);
- const zoomTo = useStoreActions((actions) => actions.zoomTo);
const currentStore = useStore();
+ const { zoomIn, zoomOut, zoomTo, fitView } = useZoomPanHelper();
useElementUpdater(elements);
@@ -157,9 +156,9 @@ const GraphView = ({
if (onLoad) {
onLoad({
fitView: (params = { padding: 0.1 }) => fitView(params),
- zoomIn: () => zoom(0.2),
- zoomOut: () => zoom(-0.2),
- zoomTo: (zoomLevel) => zoomTo(zoomLevel),
+ zoomIn,
+ zoomOut,
+ zoomTo,
project: onLoadProject(currentStore),
getElements: onLoadGetElements(currentStore),
setTransform: (transform: FlowTransform) =>
diff --git a/src/container/ReactFlow/Wrapper.tsx b/src/container/ReactFlow/Wrapper.tsx
index c5cc6de0..2081e092 100644
--- a/src/container/ReactFlow/Wrapper.tsx
+++ b/src/container/ReactFlow/Wrapper.tsx
@@ -1,10 +1,11 @@
import React, { FC } from 'react';
-import { StoreProvider, useStore } from 'easy-peasy';
+import { StoreProvider } from 'easy-peasy';
-import store, { StoreModel } from '../../store';
+import store from '../../store';
+import { useStore } from '../../store/hooks';
const Wrapper: FC = ({ children }) => {
- const easyPeasyStore = useStore
();
+ const easyPeasyStore = useStore();
const isWrapepdWithReactFlowProvider = easyPeasyStore?.getState()?.reactFlowVersion;
if (isWrapepdWithReactFlowProvider) {
diff --git a/src/container/ZoomPane/index.tsx b/src/container/ZoomPane/index.tsx
index 9fe19301..9ff0bc14 100644
--- a/src/container/ZoomPane/index.tsx
+++ b/src/container/ZoomPane/index.tsx
@@ -1,7 +1,11 @@
import React, { useEffect, useRef, ReactNode } from 'react';
+import { zoom, zoomIdentity } from 'd3-zoom';
+import { select } from 'd3-selection';
+import { clamp } from '../../utils';
+
import useResizeHandler from '../../hooks/useResizeHandler';
-import { useStoreState, useStoreActions } from '../../store/hooks';
+import { useStoreState, useStoreActions, useStore } from '../../store/hooks';
import { FlowTransform, TranslateExtent } from '../../types';
interface ZoomPaneProps {
@@ -55,14 +59,36 @@ const ZoomPane = ({
const d3Selection = useStoreState((s) => s.d3Selection);
const d3ZoomHandler = useStoreState((s) => s.d3ZoomHandler);
- const initD3 = useStoreActions((actions) => actions.initD3);
+ const initD3Zoom = useStoreActions((actions) => actions.initD3Zoom);
const updateTransform = useStoreActions((actions) => actions.updateTransform);
+ const store = useStore();
useResizeHandler(zoomPane);
useEffect(() => {
if (zoomPane.current) {
- initD3({ zoomPane: zoomPane.current, defaultPosition, defaultZoom, translateExtent });
+ // initD3: action((state, { zoomPane, defaultPosition, defaultZoom, translateExtent }) => {
+ const state = store.getState();
+ const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent;
+ const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent);
+ const selection = select(zoomPane.current as Element).call(d3ZoomInstance);
+
+ const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]);
+ const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]);
+ const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom);
+
+ const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom);
+ // selection.property('__zoom', updatedTransform);
+
+ const zoomHandler = selection.on('wheel.zoom');
+
+ d3ZoomInstance.transform(selection, updatedTransform);
+
+ initD3Zoom({
+ d3Zoom: d3ZoomInstance,
+ d3Selection: selection,
+ d3ZoomHandler: zoomHandler,
+ });
}
}, []);
diff --git a/src/hooks/useZoomPanHelper.ts b/src/hooks/useZoomPanHelper.ts
new file mode 100644
index 00000000..239e3514
--- /dev/null
+++ b/src/hooks/useZoomPanHelper.ts
@@ -0,0 +1,67 @@
+import { useCallback } 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';
+
+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);
+ }
+ }, [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,
+ };
+};
diff --git a/src/store/index.ts b/src/store/index.ts
index 263e8b2f..24685222 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -1,10 +1,9 @@
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 { zoom, zoomIdentity } from 'd3-zoom';
-import { select } from 'd3-selection';
+import { zoomIdentity } from 'd3-zoom';
-import { getDimensions, clamp } from '../utils';
+import { getDimensions } from '../utils';
import { getHandleBounds } from '../components/Nodes/utils';
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph';
@@ -26,7 +25,6 @@ import {
SetConnectionId,
NodePosUpdate,
NodeDiffUpdate,
- FitViewParams,
TranslateExtent,
SnapGrid,
} from '../types';
@@ -42,11 +40,10 @@ type NodeDimensionUpdate = {
nodeElement: HTMLDivElement;
};
-type InitD3 = {
- zoomPane: Element;
- defaultPosition: [number, number];
- defaultZoom: number;
- translateExtent?: TranslateExtent;
+type InitD3Zoom = {
+ d3Zoom: ZoomBehavior;
+ d3Selection: D3Selection;
+ d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
};
export interface StoreModel {
width: number;
@@ -119,7 +116,7 @@ export interface StoreModel {
updateSize: Action;
- initD3: Action;
+ initD3Zoom: Action;
setMinZoom: Action;
setMaxZoom: Action;
@@ -142,12 +139,6 @@ export interface StoreModel {
updateUserSelection: Action;
unsetUserSelection: Action;
- fitView: Action;
- zoomTo: Action;
- zoom: Thunk;
- zoomIn: Thunk;
- zoomOut: Thunk;
-
setMultiSelectionActive: Action;
}
@@ -397,26 +388,10 @@ export const storeModel: StoreModel = {
state.height = size.height || 500;
}),
- initD3: action((state, { zoomPane, defaultPosition, defaultZoom, translateExtent }) => {
- const currentTranslateExtent = typeof translateExtent !== 'undefined' ? translateExtent : state.translateExtent;
- const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]).translateExtent(currentTranslateExtent);
- const selection = select(zoomPane).call(d3ZoomInstance);
-
- const clampedX = clamp(defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]);
- const clampedY = clamp(defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]);
- const clampedZoom = clamp(defaultZoom, state.minZoom, state.maxZoom);
-
- const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom);
- selection.property('__zoom', updatedTransform);
-
- const defaultHandler = selection.on('wheel.zoom');
-
- state.transform[0] = clampedX;
- state.transform[1] = clampedY;
- state.transform[2] = clampedZoom;
- state.d3Zoom = d3ZoomInstance;
- state.d3Selection = selection;
- state.d3ZoomHandler = defaultHandler;
+ initD3Zoom: action((state, { d3Zoom, d3Selection, d3ZoomHandler }) => {
+ state.d3Zoom = d3Zoom;
+ state.d3Selection = d3Selection;
+ state.d3ZoomHandler = d3ZoomHandler;
state.d3Initialised = true;
}),
@@ -481,68 +456,6 @@ export const storeModel: StoreModel = {
state.elementsSelectable = elementsSelectable;
}),
- fitView: action((state, payload = { padding: 0.1 }) => {
- const { padding } = payload;
- const { nodes, width, height, d3Selection, minZoom, maxZoom } = state;
-
- 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 transform = [width / 2 - boundsCenterX * clampedZoom, height / 2 - boundsCenterY * clampedZoom];
- const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(clampedZoom);
-
- // we need to sync the d3 zoom transform with the fitted transform
- d3Selection.property('__zoom', fittedTransform);
-
- state.transform[0] = fittedTransform.x;
- state.transform[1] = fittedTransform.y;
- state.transform[2] = fittedTransform.k;
- }),
-
- zoomTo: action((state, zoomLevel) => {
- const { d3Selection, transform, minZoom, maxZoom } = state;
- const nextZoom = clamp(zoomLevel, minZoom, maxZoom);
-
- if (d3Selection) {
- // we want to zoom in and out to the center of the zoom pane
- const center = [state.width / 2, state.height / 2];
- const centerInverted = [(center[0] - transform[0]) / transform[2], (center[1] - transform[1]) / transform[2]];
- const x = center[0] - centerInverted[0] * nextZoom;
- const y = center[1] - centerInverted[1] * nextZoom;
- const zoomedTransform = zoomIdentity.translate(x, y).scale(nextZoom);
-
- // we need to sync the d3 zoom transform with the zoomed transform
- d3Selection.property('__zoom', zoomedTransform);
-
- state.transform[0] = zoomedTransform.x;
- state.transform[1] = zoomedTransform.y;
- state.transform[2] = zoomedTransform.k;
- }
- }),
-
- zoom: thunk((actions, amount, helpers) => {
- const { transform } = helpers.getState();
- const nextZoom = transform[2] + amount;
-
- actions.zoomTo(nextZoom);
- }),
-
- zoomIn: thunk((actions) => {
- actions.zoom(0.2);
- }),
-
- zoomOut: thunk((actions) => {
- actions.zoom(-0.2);
- }),
-
setMultiSelectionActive: action((state, isActive) => {
state.multiSelectionActive = isActive;
}),
diff --git a/src/utils/graph.ts b/src/utils/graph.ts
index d39c8f7e..1870a0fb 100644
--- a/src/utils/graph.ts
+++ b/src/utils/graph.ts
@@ -1,7 +1,6 @@
import { Store } from 'easy-peasy';
-
-import store, { StoreModel } from '../store';
-import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, FitViewParams, Box, Connection } from '../types';
+import { StoreModel } from '../store';
+import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, Box, Connection } from '../types';
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element;
@@ -106,12 +105,6 @@ export const onLoadProject = (currentStore: Store) => {
};
};
-export const project = (position: XYPosition): XYPosition => {
- const { transform, snapToGrid, snapGrid } = store.getState();
-
- return pointToRendererPoint(position, transform, snapToGrid, snapGrid);
-};
-
export const parseElement = (element: Node | Edge): Node | Edge => {
if (!element.id) {
throw new Error('All nodes and edges need to have an id.');
@@ -227,18 +220,6 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
});
};
-export const fitView = (params: FitViewParams = { padding: 0.1 }): void => {
- store.getActions().fitView(params);
-};
-
-const zoom = (amount: number): void => {
- store.getActions().zoom(amount);
-};
-
-export const zoomIn = (): void => zoom(0.2);
-
-export const zoomOut = (): void => zoom(-0.2);
-
const parseElements = (nodes: Node[], edges: Edge[]): Elements => {
return [
...nodes.map((node) => {
@@ -258,9 +239,3 @@ export const onLoadGetElements = (currentStore: Store) => {
return parseElements(nodes, edges);
};
};
-
-export const getElements = (): Elements => {
- const { nodes = [], edges = [] } = store.getState();
-
- return parseElements(nodes, edges);
-};