diff --git a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx index f80b2971..66cab6a0 100644 --- a/examples/vite-app/src/examples/InteractiveMinimap/index.tsx +++ b/examples/vite-app/src/examples/InteractiveMinimap/index.tsx @@ -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 > diff --git a/packages/core/src/container/ZoomPane/index.tsx b/packages/core/src/container/ZoomPane/index.tsx index cde4ec47..a5783a7a 100644 --- a/packages/core/src/container/ZoomPane/index.tsx +++ b/packages/core/src/container/ZoomPane/index.tsx @@ -173,9 +173,12 @@ const ZoomPane = ({ useEffect(() => { if (d3Zoom) { d3Zoom.on('start', (event: D3ZoomEvent) => { + 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) => { + if (!event.sourceEvent) { + return null; + } const { onViewportChangeEnd } = store.getState(); isZoomingOrPanning.current = false; diff --git a/packages/interactive-minimap/src/MiniMap.tsx b/packages/interactive-minimap/src/MiniMap.tsx index b7ef604c..e5a7927e 100644 --- a/packages/interactive-minimap/src/MiniMap.tsx +++ b/packages/interactive-minimap/src/MiniMap.tsx @@ -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(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({ 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) => { - console.log(event); - }); - }, []); + const selection = select(svg.current as Element); + + const zoomHandler = zoom() + .on('start', (event: D3ZoomEvent) => { + 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) => { + 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) => { + 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 (