diff --git a/examples/vite-app/src/App/index.tsx b/examples/vite-app/src/App/index.tsx
index 31d41fd6..f0e32fb8 100644
--- a/examples/vite-app/src/App/index.tsx
+++ b/examples/vite-app/src/App/index.tsx
@@ -16,6 +16,7 @@ import Empty from '../examples/Empty';
import FloatingEdges from '../examples/FloatingEdges';
import Hidden from '../examples/Hidden';
import Interaction from '../examples/Interaction';
+import Intersection from '../examples/Intersection';
import Layouting from '../examples/Layouting';
import MultiFlows from '../examples/MultiFlows';
import NestedNodes from '../examples/NestedNodes';
@@ -132,6 +133,11 @@ const routes: IRoute[] = [
path: '/interaction',
component: Interaction,
},
+ {
+ name: 'Intersection',
+ path: '/intersection',
+ component: Intersection,
+ },
{
name: 'Interactive Minimap',
path: '/interactive-minimap',
diff --git a/examples/vite-app/src/examples/Intersection/index.tsx b/examples/vite-app/src/examples/Intersection/index.tsx
new file mode 100644
index 00000000..1f82b308
--- /dev/null
+++ b/examples/vite-app/src/examples/Intersection/index.tsx
@@ -0,0 +1,104 @@
+import { useCallback, MouseEvent } from 'react';
+import ReactFlow, {
+ MiniMap,
+ Background,
+ Controls,
+ ReactFlowProvider,
+ Node,
+ Edge,
+ useReactFlow,
+ useNodesState,
+} from 'reactflow';
+
+import './style.css';
+
+const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
+const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
+
+const initialNodes: Node[] = [
+ {
+ id: '1',
+ type: 'input',
+ data: { label: 'Node 1' },
+ position: { x: 0, y: 0 },
+ className: 'light',
+ style: {
+ width: 200,
+ height: 100,
+ },
+ },
+ {
+ id: '2',
+ data: { label: 'Node 2' },
+ position: { x: 0, y: 150 },
+ className: 'light',
+ },
+ {
+ id: '3',
+ data: { label: 'Node 3' },
+ position: { x: 250, y: 0 },
+ className: 'light',
+ },
+ {
+ id: '4',
+ data: { label: 'Node' },
+ position: { x: 350, y: 150 },
+ style: {
+ width: 50,
+ height: 50,
+ },
+ className: 'light',
+ },
+];
+
+const initialEdges: Edge[] = [];
+
+const defaultEdgeOptions = { zIndex: 0 };
+
+const BasicFlow = () => {
+ const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
+ const { getIntersectingNodes, isNodeIntersecting } = useReactFlow();
+
+ const onNodeDrag = useCallback((_: MouseEvent, node: Node) => {
+ const intersections = getIntersectingNodes(node).map((n) => n.id);
+ const isIntersecting = isNodeIntersecting(node, { x: 0, y: 0, width: 100, height: 100 });
+
+ console.log(isIntersecting);
+
+ setNodes((ns) =>
+ ns.map((n) => ({
+ ...n,
+ className: intersections.includes(n.id) ? 'highlight' : '',
+ }))
+ );
+ }, []);
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default function App() {
+ return (
+
+
+
+ );
+}
diff --git a/examples/vite-app/src/examples/Intersection/style.css b/examples/vite-app/src/examples/Intersection/style.css
new file mode 100644
index 00000000..16b4cc4c
--- /dev/null
+++ b/examples/vite-app/src/examples/Intersection/style.css
@@ -0,0 +1,4 @@
+.react-flow__node.highlight {
+ background-color: #ff5050;
+ color: white;
+}
diff --git a/packages/core/src/hooks/useReactFlow.ts b/packages/core/src/hooks/useReactFlow.ts
index 77415b9b..0801a772 100644
--- a/packages/core/src/hooks/useReactFlow.ts
+++ b/packages/core/src/hooks/useReactFlow.ts
@@ -13,8 +13,10 @@ import type {
EdgeRemoveChange,
NodeChange,
Node,
+ Rect,
} from '../types';
import { getConnectedEdges } from '../utils/graph';
+import { getOverlappingArea, isRectObject, nodeToRect } from '../utils';
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export default function useReactFlow(): ReactFlowInstance {
@@ -191,6 +193,63 @@ export default function useReactFlow(): ReactFlo
}
}, []);
+ const getNodeRect = useCallback(
+ (
+ nodeOrRect: (Partial> & { id: Node['id'] }) | Rect
+ ): [Rect | null, Node | null | undefined, boolean] => {
+ const isRect = isRectObject(nodeOrRect);
+ const node = isRect ? null : store.getState().nodeInternals.get(nodeOrRect.id);
+
+ if (!isRect && !node) {
+ [null, null, isRect];
+ }
+
+ const nodeRect = isRect ? nodeOrRect : nodeToRect(node!);
+
+ return [nodeRect, node, isRect];
+ },
+ []
+ );
+
+ const getIntersectingNodes = useCallback>(
+ (nodeOrRect, partially = true, nodes) => {
+ const [nodeRect, node, isRect] = getNodeRect(nodeOrRect);
+
+ if (!nodeRect) {
+ return [];
+ }
+
+ return (nodes || Array.from(store.getState().nodeInternals.values())).filter((n) => {
+ if (!isRect && (n.id === node!.id || !n.positionAbsolute)) {
+ return false;
+ }
+
+ const currNodeRect = nodeToRect(n);
+ const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
+ const partiallyVisible = partially && overlappingArea > 0;
+
+ return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
+ });
+ },
+ []
+ );
+
+ const isNodeIntersecting = useCallback>(
+ (nodeOrRect, area, partially = true) => {
+ const [nodeRect] = getNodeRect(nodeOrRect);
+
+ if (!nodeRect) {
+ return false;
+ }
+
+ const overlappingArea = getOverlappingArea(nodeRect, area);
+ const partiallyVisible = partially && overlappingArea > 0;
+
+ return partiallyVisible || overlappingArea >= nodeOrRect.width! * nodeOrRect.height!;
+ },
+ []
+ );
+
return useMemo(() => {
return {
...viewportHelper,
@@ -204,6 +263,8 @@ export default function useReactFlow(): ReactFlo
addEdges,
toObject,
deleteElements,
+ getIntersectingNodes,
+ isNodeIntersecting,
};
}, [
viewportHelper,
@@ -217,5 +278,7 @@ export default function useReactFlow(): ReactFlo
addEdges,
toObject,
deleteElements,
+ getIntersectingNodes,
+ isNodeIntersecting,
]);
}
diff --git a/packages/core/src/types/instance.ts b/packages/core/src/types/instance.ts
index 0c7200d2..7ff85606 100644
--- a/packages/core/src/types/instance.ts
+++ b/packages/core/src/types/instance.ts
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-namespace */
-import { ViewportHelperFunctions, Viewport, Node, Edge } from '.';
+import { ViewportHelperFunctions, Viewport, Node, Edge, Rect } from '.';
export type ReactFlowJsonObject = {
nodes: Node[];
@@ -9,8 +9,8 @@ export type ReactFlowJsonObject = {
};
export type DeleteElementsOptions = {
- nodes?: (Partial & { id: Node['id'] })[],
- edges?: (Partial & { id: Edge['id'] })[]
+ nodes?: (Partial & { id: Node['id'] })[];
+ edges?: (Partial & { id: Edge['id'] })[];
};
export namespace Instance {
export type GetNodes = () => Node[];
@@ -27,6 +27,16 @@ export namespace Instance {
export type AddEdges = (payload: Edge[] | Edge) => void;
export type ToObject = () => ReactFlowJsonObject;
export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => void;
+ export type GetIntersectingNodes = (
+ node: (Partial> & { id: Node['id'] }) | Rect,
+ partially?: boolean,
+ nodes?: Node[]
+ ) => Node[];
+ export type IsNodeIntersecting = (
+ node: (Partial> & { id: Node['id'] }) | Rect,
+ area: Rect,
+ partially?: boolean
+ ) => boolean;
}
export type ReactFlowInstance = {
@@ -40,5 +50,7 @@ export type ReactFlowInstance = {
getEdge: Instance.GetEdge;
toObject: Instance.ToObject;
deleteElements: Instance.DeleteElements;
+ getIntersectingNodes: Instance.GetIntersectingNodes;
+ isNodeIntersecting: Instance.IsNodeIntersecting;
viewportInitialized: boolean;
} & Omit;
diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts
index 123fd13e..78569276 100644
--- a/packages/core/src/utils/graph.ts
+++ b/packages/core/src/utils/graph.ts
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Selection as D3Selection } from 'd3';
-import { boxToRect, clamp, devWarn, getBoundsOfBoxes, rectToBox } from '../utils';
+import { boxToRect, clamp, devWarn, getBoundsOfBoxes, getOverlappingArea, rectToBox } from '../utils';
import type { Node, Edge, Connection, EdgeMarkerType, Transform, XYPosition, Rect, NodeInternals } from '../types';
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
@@ -161,12 +161,12 @@ export const getNodesInside = (
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
excludeNonSelectableNodes = false
): Node[] => {
- const rBox = rectToBox({
+ const paneRect = {
x: (rect.x - tx) / tScale,
y: (rect.y - ty) / tScale,
width: rect.width / tScale,
height: rect.height / tScale,
- });
+ };
const visibleNodes: Node[] = [];
@@ -177,10 +177,8 @@ export const getNodesInside = (
return false;
}
- const nBox = rectToBox({ ...positionAbsolute, width: width || 0, height: height || 0 });
- const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x));
- const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
- const overlappingArea = Math.ceil(xOverlap * yOverlap);
+ const nodeRect = { ...positionAbsolute, width: width || 0, height: height || 0 };
+ const overlappingArea = getOverlappingArea(paneRect, nodeRect);
const notInitialized =
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null;
diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts
index c3afe773..02cff5a0 100644
--- a/packages/core/src/utils/index.ts
+++ b/packages/core/src/utils/index.ts
@@ -1,4 +1,4 @@
-import type { Dimensions, XYPosition, CoordinateExtent, Box, Rect } from '../types';
+import type { Dimensions, Node, XYPosition, CoordinateExtent, Box, Rect } from '../types';
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
width: node.offsetWidth,
@@ -36,9 +36,25 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
height: y2 - y,
});
+export const nodeToRect = (node: Node): Rect => ({
+ ...(node.positionAbsolute || { x: 0, y: 0 }),
+ width: node.width || 0,
+ height: node.height || 0,
+});
+
export const getBoundsOfRects = (rect1: Rect, rect2: Rect): Rect =>
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
+export const getOverlappingArea = (rectA: Rect, rectB: Rect): number => {
+ const xOverlap = Math.max(0, Math.min(rectA.x + rectA.width, rectB.x + rectB.width) - Math.max(rectA.x, rectB.x));
+ const yOverlap = Math.max(0, Math.min(rectA.y + rectA.height, rectB.y + rectB.height) - Math.max(rectA.y, rectB.y));
+
+ return Math.ceil(xOverlap * yOverlap);
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+export const isRectObject = (obj: any): obj is Rect => !!obj.width && !!obj.height && !!obj.x && !!obj.y;
+
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);