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 logToObject = () => console.log(rfInstance.toObject());
const resetTransform = () => rfInstance.setTransform({ x: 0, y: 0, zoom: 1 });
return ( return (
<ReactFlow <ReactFlow
elements={elements} elements={elements}
@@ -57,6 +59,9 @@ const BasicFlow = () => {
<Background variant="lines" /> <Background variant="lines" />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}> <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 }}> <button onClick={updatePos} style={{ marginRight: 5 }}>
change pos change pos
</button> </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 cc from 'classcat';
import { useStoreState, useStoreActions } from '../../store/hooks'; import { useStoreState, useStoreActions } from '../../store/hooks';
@@ -39,56 +39,47 @@ const Controls = ({
const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable); const isInteractive = useStoreState((s) => s.nodesDraggable && s.nodesConnectable && s.elementsSelectable);
const mapClasses = cc(['react-flow__controls', className]); 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 ( return (
<div className={mapClasses} style={style}> <div className={mapClasses} style={style}>
{showZoom && ( {showZoom && (
<> <>
<div <div className="react-flow__controls-button react-flow__controls-zoomin" onClick={onZoomInHandler}>
className="react-flow__controls-button react-flow__controls-zoomin"
onClick={() => {
zoomIn();
if (onZoomIn) {
onZoomIn();
}
}}
>
<PlusIcon /> <PlusIcon />
</div> </div>
<div <div className="react-flow__controls-button react-flow__controls-zoomout" onClick={onZoomOutHandler}>
className="react-flow__controls-button react-flow__controls-zoomout"
onClick={() => {
zoomOut();
if (onZoomOut) {
onZoomOut();
}
}}
>
<MinusIcon /> <MinusIcon />
</div> </div>
</> </>
)} )}
{showFitView && ( {showFitView && (
<div <div className="react-flow__controls-button react-flow__controls-fitview" onClick={onFitViewHandler}>
className="react-flow__controls-button react-flow__controls-fitview"
onClick={() => {
fitView({ padding: 0.1 });
if (onFitView) {
onFitView();
}
}}
>
<FitviewIcon /> <FitviewIcon />
</div> </div>
)} )}
{showInteractive && ( {showInteractive && (
<div <div
className="react-flow__controls-button react-flow__controls-interactive" className="react-flow__controls-button react-flow__controls-interactive"
onClick={() => { onClick={onInteractiveChangeHandler}
setInteractive(!isInteractive);
if (onInteractiveChange) {
onInteractiveChange(!isInteractive);
}
}}
> >
{isInteractive ? <UnlockIcon /> : <LockIcon />} {isInteractive ? <UnlockIcon /> : <LockIcon />}
</div> </div>
+5 -8
View File
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, memo, CSSProperties, MouseEvent, WheelEvent } from 'react'; 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 FlowRenderer from '../FlowRenderer';
import NodeRenderer from '../NodeRenderer'; import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer'; import EdgeRenderer from '../EdgeRenderer';
@@ -132,7 +132,6 @@ const GraphView = ({
onPaneContextMenu, onPaneContextMenu,
}: GraphViewProps) => { }: GraphViewProps) => {
const isInitialised = useRef<boolean>(false); const isInitialised = useRef<boolean>(false);
const d3Zoom = useStoreState((state) => state.d3Zoom);
const setOnConnect = useStoreActions((actions) => actions.setOnConnect); const setOnConnect = useStoreActions((actions) => actions.setOnConnect);
const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart); const setOnConnectStart = useStoreActions((actions) => actions.setOnConnectStart);
const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop); const setOnConnectStop = useStoreActions((actions) => actions.setOnConnectStop);
@@ -142,34 +141,32 @@ const GraphView = ({
const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable); const setNodesDraggable = useStoreActions((actions) => actions.setNodesDraggable);
const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable); const setNodesConnectable = useStoreActions((actions) => actions.setNodesConnectable);
const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable); const setElementsSelectable = useStoreActions((actions) => actions.setElementsSelectable);
const setInitTransform = useStoreActions((actions) => actions.setInitTransform);
const setMinZoom = useStoreActions((actions) => actions.setMinZoom); const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom); const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent); const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
const currentStore = useStore(); const currentStore = useStore();
const { zoomIn, zoomOut, zoomTo, fitView } = useZoomPanHelper(); const { zoomIn, zoomOut, zoomTo, transform, fitView } = useZoomPanHelper();
useElementUpdater(elements); useElementUpdater(elements);
useEffect(() => { useEffect(() => {
if (!isInitialised.current && d3Zoom) { if (!isInitialised.current && zoomIn && zoomOut && zoomTo && transform && fitView) {
if (onLoad) { if (onLoad) {
onLoad({ onLoad({
fitView: (params = { padding: 0.1 }) => fitView(params), fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn, zoomIn,
zoomOut, zoomOut,
zoomTo, zoomTo,
setTransform: transform,
project: onLoadProject(currentStore), project: onLoadProject(currentStore),
getElements: onLoadGetElements(currentStore), getElements: onLoadGetElements(currentStore),
setTransform: (transform: FlowTransform) =>
setInitTransform({ x: transform.x, y: transform.y, k: transform.zoom }),
toObject: onLoadToObject(currentStore), toObject: onLoadToObject(currentStore),
}); });
} }
isInitialised.current = true; isInitialised.current = true;
} }
}, [d3Zoom, onLoad]); }, [onLoad, zoomIn, zoomOut, zoomTo, transform, fitView]);
useEffect(() => { useEffect(() => {
if (onConnect) { if (onConnect) {
+1 -1
View File
@@ -123,7 +123,7 @@ const ZoomPane = ({
d3Zoom.on('zoom', null); d3Zoom.on('zoom', null);
} else { } else {
d3Zoom.on('zoom', (event: any) => { d3Zoom.on('zoom', (event: any) => {
updateTransform(event.transform); updateTransform([event.transform.x, event.transform.y, event.transform.k]);
if (onMove) { if (onMove) {
const flowTransform = eventToFlowTransform(event.transform); 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 { zoomIdentity } from 'd3-zoom';
import { useStoreState, useStore } from '../store/hooks'; import { useStoreState, useStore } from '../store/hooks';
import { clamp } from '../utils'; import { clamp } from '../utils';
import { getRectOfNodes } from '../utils/graph'; 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 () => { export default () => {
const store = useStore(); const store = useStore();
const d3Zoom = useStoreState((s) => s.d3Zoom); const d3Zoom = useStoreState((s) => s.d3Zoom);
const d3Selection = useStoreState((s) => s.d3Selection); const d3Selection = useStoreState((s) => s.d3Selection);
const zoomIn = useCallback(() => { const zoomPanHelperFunctions = useMemo(() => {
if (d3Selection) { if (d3Selection && d3Zoom) {
d3Zoom?.scaleBy(d3Selection, 1.2); 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]); }, [d3Zoom, d3Selection]);
const zoomOut = useCallback(() => { return zoomPanHelperFunctions;
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,
};
}; };
+1
View File
@@ -9,6 +9,7 @@ export { getSmoothStepPath } from './components/Edges/SmoothStepEdge';
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils'; export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
export { isNode, isEdge, removeElements, addEdge, getOutgoers, getIncomers, getConnectedEdges } from './utils/graph'; export { isNode, isEdge, removeElements, addEdge, getOutgoers, getIncomers, getConnectedEdges } from './utils/graph';
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper';
export * from './additional-components'; export * from './additional-components';
export * from './store/hooks'; export * from './store/hooks';
+7 -28
View File
@@ -1,12 +1,11 @@
import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy'; import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'easy-peasy';
import isEqual from 'fast-deep-equal'; import isEqual from 'fast-deep-equal';
import { Selection as D3Selection, ZoomBehavior } from 'd3'; import { Selection as D3Selection, ZoomBehavior } from 'd3';
import { zoomIdentity } from 'd3-zoom';
import { getDimensions } from '../utils'; import { getDimensions } from '../utils';
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph';
import { getHandleBounds } from '../components/Nodes/utils'; import { getHandleBounds } from '../components/Nodes/utils';
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge } from '../utils/graph';
import { import {
ElementId, ElementId,
Elements, Elements,
@@ -29,12 +28,6 @@ import {
SnapGrid, SnapGrid,
} from '../types'; } from '../types';
type TransformXYK = {
x: number;
y: number;
k: number;
};
type NodeDimensionUpdate = { type NodeDimensionUpdate = {
id: ElementId; id: ElementId;
nodeElement: HTMLDivElement; nodeElement: HTMLDivElement;
@@ -110,9 +103,7 @@ export interface StoreModel {
setSelectedElements: Action<StoreModel, Elements | Node | Edge>; setSelectedElements: Action<StoreModel, Elements | Node | Edge>;
addSelectedElements: Thunk<StoreModel, Elements | Node | Edge>; addSelectedElements: Thunk<StoreModel, Elements | Node | Edge>;
updateTransform: Action<StoreModel, TransformXYK>; updateTransform: Action<StoreModel, Transform>;
setInitTransform: Action<StoreModel, TransformXYK>;
updateSize: Action<StoreModel, Dimensions>; 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 })), viewportBox: computed((state) => ({ x: 0, y: 0, width: state.width, height: state.height })),
transform: [0, 0, 1], transform: [0, 0, 1],
elements: [], elements: [],
nodes: computed((state) => state.elements.filter((el) => isNode(el)) as Node[]), nodes: computed((state) => state.elements.filter(isNode)),
edges: computed((state) => state.elements.filter((el) => isEdge(el)) as Edge[]), edges: computed((state) => state.elements.filter(isEdge)),
selectedElements: null, selectedElements: null,
selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 }, selectedNodesBbox: { x: 0, y: 0, width: 0, height: 0 },
@@ -363,21 +354,9 @@ export const storeModel: StoreModel = {
}), }),
updateTransform: action((state, transform) => { updateTransform: action((state, transform) => {
state.transform[0] = transform.x; state.transform[0] = transform[0];
state.transform[1] = transform.y; state.transform[1] = transform[1];
state.transform[2] = transform.k; state.transform[2] = transform[2];
}),
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);
}
}), }),
updateSize: action((state, size) => { updateSize: action((state, size) => {