refactor(zoomPanHelper): export and simplify

This commit is contained in:
moklick
2020-11-09 19:30:21 +01:00
parent c7bf8ffbcc
commit e1ae70e779
7 changed files with 89 additions and 123 deletions
+5
View File
@@ -41,6 +41,8 @@ const BasicFlow = () => {
const logToObject = () => console.log(rfInstance.toObject());
const resetTransform = () => rfInstance.setTransform({ x: 0, y: 0, zoom: 1 });
return (
<ReactFlow
elements={elements}
@@ -57,6 +59,9 @@ const BasicFlow = () => {
<Background variant="lines" />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
+25 -34
View File
@@ -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 (
<div className={mapClasses} style={style}>
{showZoom && (
<>
<div
className="react-flow__controls-button react-flow__controls-zoomin"
onClick={() => {
zoomIn();
if (onZoomIn) {
onZoomIn();
}
}}
>
<div className="react-flow__controls-button react-flow__controls-zoomin" onClick={onZoomInHandler}>
<PlusIcon />
</div>
<div
className="react-flow__controls-button react-flow__controls-zoomout"
onClick={() => {
zoomOut();
if (onZoomOut) {
onZoomOut();
}
}}
>
<div className="react-flow__controls-button react-flow__controls-zoomout" onClick={onZoomOutHandler}>
<MinusIcon />
</div>
</>
)}
{showFitView && (
<div
className="react-flow__controls-button react-flow__controls-fitview"
onClick={() => {
fitView({ padding: 0.1 });
if (onFitView) {
onFitView();
}
}}
>
<div className="react-flow__controls-button react-flow__controls-fitview" onClick={onFitViewHandler}>
<FitviewIcon />
</div>
)}
{showInteractive && (
<div
className="react-flow__controls-button react-flow__controls-interactive"
onClick={() => {
setInteractive(!isInteractive);
if (onInteractiveChange) {
onInteractiveChange(!isInteractive);
}
}}
onClick={onInteractiveChangeHandler}
>
{isInteractive ? <UnlockIcon /> : <LockIcon />}
</div>
+5 -8
View File
@@ -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<boolean>(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) {
+1 -1
View File
@@ -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);
+45 -52
View File
@@ -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;
};
+1
View File
@@ -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';
+7 -28
View File
@@ -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<StoreModel, Elements | Node | Edge>;
addSelectedElements: Thunk<StoreModel, Elements | Node | Edge>;
updateTransform: Action<StoreModel, TransformXYK>;
setInitTransform: Action<StoreModel, TransformXYK>;
updateTransform: Action<StoreModel, Transform>;
updateSize: Action<StoreModel, Dimensions>;
@@ -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) => {