feat(interactive-minimap): add zoom and pan

This commit is contained in:
moklick
2022-10-31 17:55:32 +01:00
parent 8323ee0c91
commit 322ea0ebed
3 changed files with 86 additions and 14 deletions

View File

@@ -19,25 +19,25 @@ const initialNodes: Node[] = [
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
position: { x: 0, y: 0 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
position: { x: 0, y: 200 },
className: 'light',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 400, y: 100 },
position: { x: 200, y: 200 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 400, y: 200 },
position: { x: 200, y: 400 },
className: 'light',
},
];
@@ -88,9 +88,9 @@ const BasicFlow = () => {
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
fitView
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
fitView
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />

View File

@@ -173,9 +173,12 @@ const ZoomPane = ({
useEffect(() => {
if (d3Zoom) {
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (!event.sourceEvent) {
return null;
}
const { onViewportChangeStart } = store.getState();
isZoomingOrPanning.current = true;
if (event.sourceEvent?.type === 'mousedown') {
store.setState({ paneDragging: true });
}
@@ -194,6 +197,9 @@ const ZoomPane = ({
useEffect(() => {
if (d3Zoom) {
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (!event.sourceEvent) {
return null;
}
const { onViewportChangeEnd } = store.getState();
isZoomingOrPanning.current = false;

View File

@@ -2,9 +2,19 @@
import { memo, useEffect, useRef } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { zoom, D3ZoomEvent } from 'd3-zoom';
import { zoom, D3ZoomEvent, zoomIdentity } from 'd3-zoom';
import { select } from 'd3-selection';
import { useStore, getRectOfNodes, ReactFlowState, Rect, Panel, getBoundsOfRects } from '@reactflow/core';
import {
useStore,
getRectOfNodes,
ReactFlowState,
Rect,
Panel,
getBoundsOfRects,
useStoreApi,
useReactFlow,
XYPosition,
} from '@reactflow/core';
import MiniMapNode from './MiniMapNode';
import { MiniMapProps, GetMiniMapNodeAttribute } from './types';
@@ -46,7 +56,9 @@ function MiniMap({
maskColor = 'rgb(240, 242, 243, 0.7)',
position = 'bottom-right',
}: MiniMapProps) {
const store = useStoreApi();
const svg = useRef<SVGSVGElement>(null);
const initialized = useRef(false);
const { boundingRect, viewBB, nodes, rfId } = useStore(selector, shallow);
const elementWidth = (style?.width as number) ?? defaultWidth;
const elementHeight = (style?.height as number) ?? defaultHeight;
@@ -65,15 +77,69 @@ function MiniMap({
const height = viewHeight + offset * 2;
const shapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision';
const labelledBy = `${ARIA_LABEL_KEY}-${rfId}`;
const { setCenter } = useReactFlow();
const startTransform = useRef<XYPosition>({ x: 0, y: 0 });
const viewScaleRef = useRef(0);
const viewScaleWRef = useRef(0);
const viewScaleHRef = useRef(0);
viewScaleRef.current = viewScale;
viewScaleWRef.current = scaledWidth;
viewScaleHRef.current = scaledHeight;
useEffect(() => {
const d3ZoomInstance = zoom();
const selection = select(svg.current as Element).call(d3ZoomInstance);
if (!initialized.current && svg.current) {
const bounds = svg.current.getBoundingClientRect() || { left: 0, top: 0 };
d3ZoomInstance.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
console.log(event);
});
}, []);
const selection = select(svg.current as Element);
const zoomHandler = zoom()
.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
const { transform } = store.getState();
const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current;
const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current;
startTransform.current = {
x: px - transform[0] / transform[2],
y: py - transform[1] / transform[2],
};
})
.on('zoom.wheel', (event: D3ZoomEvent<HTMLDivElement, any>) => {
const { transform, d3Selection, d3Zoom } = store.getState();
if (event.sourceEvent.type !== 'wheel' || !d3Selection || !d3Zoom) {
return;
}
const pinchDelta =
-event.sourceEvent.deltaY *
(event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) *
10;
const zoom = transform[2] * Math.pow(2, pinchDelta);
d3Zoom.scaleTo(d3Selection, zoom, [startTransform.current.x, startTransform.current.y]);
})
.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (event.sourceEvent.type !== 'mousemove') {
return;
}
const { transform, d3Selection, d3Zoom } = store.getState();
const px = (event.sourceEvent.clientX - bounds.left) * viewScaleRef.current;
const py = (event.sourceEvent.clientY - bounds.top) * viewScaleRef.current;
const position = {
x: (px - startTransform.current.x) * transform[2],
y: (py - startTransform.current.y) * transform[2],
};
const nextTransform = zoomIdentity.translate(position.x, position.y).scale(transform[2]);
d3Zoom!.transform(d3Selection!, nextTransform);
});
selection.call(zoomHandler);
initialized.current = true;
}
}, [setCenter]);
return (
<Panel position={position} style={style} className={cc(['react-flow__minimap', className])}>