feat(useReactFlow): add intersection helpers
This commit is contained in:
@@ -16,6 +16,7 @@ import Empty from '../examples/Empty';
|
|||||||
import FloatingEdges from '../examples/FloatingEdges';
|
import FloatingEdges from '../examples/FloatingEdges';
|
||||||
import Hidden from '../examples/Hidden';
|
import Hidden from '../examples/Hidden';
|
||||||
import Interaction from '../examples/Interaction';
|
import Interaction from '../examples/Interaction';
|
||||||
|
import Intersection from '../examples/Intersection';
|
||||||
import Layouting from '../examples/Layouting';
|
import Layouting from '../examples/Layouting';
|
||||||
import MultiFlows from '../examples/MultiFlows';
|
import MultiFlows from '../examples/MultiFlows';
|
||||||
import NestedNodes from '../examples/NestedNodes';
|
import NestedNodes from '../examples/NestedNodes';
|
||||||
@@ -132,6 +133,11 @@ const routes: IRoute[] = [
|
|||||||
path: '/interaction',
|
path: '/interaction',
|
||||||
component: Interaction,
|
component: Interaction,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Intersection',
|
||||||
|
path: '/intersection',
|
||||||
|
component: Intersection,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Interactive Minimap',
|
name: 'Interactive Minimap',
|
||||||
path: '/interactive-minimap',
|
path: '/interactive-minimap',
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={initialEdges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
onNodeDragStop={onNodeDragStop}
|
||||||
|
onNodeDrag={onNodeDrag}
|
||||||
|
className="react-flow-basic-example"
|
||||||
|
minZoom={0.2}
|
||||||
|
maxZoom={4}
|
||||||
|
fitView
|
||||||
|
defaultEdgeOptions={defaultEdgeOptions}
|
||||||
|
selectNodesOnDrag={false}
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
<MiniMap />
|
||||||
|
<Controls />
|
||||||
|
</ReactFlow>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<BasicFlow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
.react-flow__node.highlight {
|
||||||
|
background-color: #ff5050;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
@@ -13,8 +13,10 @@ import type {
|
|||||||
EdgeRemoveChange,
|
EdgeRemoveChange,
|
||||||
NodeChange,
|
NodeChange,
|
||||||
Node,
|
Node,
|
||||||
|
Rect,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getConnectedEdges } from '../utils/graph';
|
import { getConnectedEdges } from '../utils/graph';
|
||||||
|
import { getOverlappingArea, isRectObject, nodeToRect } from '../utils';
|
||||||
|
|
||||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData> {
|
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData> {
|
||||||
@@ -191,6 +193,63 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const getNodeRect = useCallback(
|
||||||
|
(
|
||||||
|
nodeOrRect: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect
|
||||||
|
): [Rect | null, Node<NodeData> | 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<Instance.GetIntersectingNodes<NodeData>>(
|
||||||
|
(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<Instance.IsNodeIntersecting<NodeData>>(
|
||||||
|
(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 useMemo(() => {
|
||||||
return {
|
return {
|
||||||
...viewportHelper,
|
...viewportHelper,
|
||||||
@@ -204,6 +263,8 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
|||||||
addEdges,
|
addEdges,
|
||||||
toObject,
|
toObject,
|
||||||
deleteElements,
|
deleteElements,
|
||||||
|
getIntersectingNodes,
|
||||||
|
isNodeIntersecting,
|
||||||
};
|
};
|
||||||
}, [
|
}, [
|
||||||
viewportHelper,
|
viewportHelper,
|
||||||
@@ -217,5 +278,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
|||||||
addEdges,
|
addEdges,
|
||||||
toObject,
|
toObject,
|
||||||
deleteElements,
|
deleteElements,
|
||||||
|
getIntersectingNodes,
|
||||||
|
isNodeIntersecting,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
/* eslint-disable @typescript-eslint/no-namespace */
|
/* eslint-disable @typescript-eslint/no-namespace */
|
||||||
import { ViewportHelperFunctions, Viewport, Node, Edge } from '.';
|
import { ViewportHelperFunctions, Viewport, Node, Edge, Rect } from '.';
|
||||||
|
|
||||||
export type ReactFlowJsonObject<NodeData = any, EdgeData = any> = {
|
export type ReactFlowJsonObject<NodeData = any, EdgeData = any> = {
|
||||||
nodes: Node<NodeData>[];
|
nodes: Node<NodeData>[];
|
||||||
@@ -9,8 +9,8 @@ export type ReactFlowJsonObject<NodeData = any, EdgeData = any> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type DeleteElementsOptions = {
|
export type DeleteElementsOptions = {
|
||||||
nodes?: (Partial<Node> & { id: Node['id'] })[],
|
nodes?: (Partial<Node> & { id: Node['id'] })[];
|
||||||
edges?: (Partial<Edge> & { id: Edge['id'] })[]
|
edges?: (Partial<Edge> & { id: Edge['id'] })[];
|
||||||
};
|
};
|
||||||
export namespace Instance {
|
export namespace Instance {
|
||||||
export type GetNodes<NodeData> = () => Node<NodeData>[];
|
export type GetNodes<NodeData> = () => Node<NodeData>[];
|
||||||
@@ -27,6 +27,16 @@ export namespace Instance {
|
|||||||
export type AddEdges<EdgeData> = (payload: Edge<EdgeData>[] | Edge<EdgeData>) => void;
|
export type AddEdges<EdgeData> = (payload: Edge<EdgeData>[] | Edge<EdgeData>) => void;
|
||||||
export type ToObject<NodeData = any, EdgeData = any> = () => ReactFlowJsonObject<NodeData, EdgeData>;
|
export type ToObject<NodeData = any, EdgeData = any> = () => ReactFlowJsonObject<NodeData, EdgeData>;
|
||||||
export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => void;
|
export type DeleteElements = ({ nodes, edges }: DeleteElementsOptions) => void;
|
||||||
|
export type GetIntersectingNodes<NodeData> = (
|
||||||
|
node: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect,
|
||||||
|
partially?: boolean,
|
||||||
|
nodes?: Node<NodeData>[]
|
||||||
|
) => Node<NodeData>[];
|
||||||
|
export type IsNodeIntersecting<NodeData> = (
|
||||||
|
node: (Partial<Node<NodeData>> & { id: Node['id'] }) | Rect,
|
||||||
|
area: Rect,
|
||||||
|
partially?: boolean
|
||||||
|
) => boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ReactFlowInstance<NodeData = any, EdgeData = any> = {
|
export type ReactFlowInstance<NodeData = any, EdgeData = any> = {
|
||||||
@@ -40,5 +50,7 @@ export type ReactFlowInstance<NodeData = any, EdgeData = any> = {
|
|||||||
getEdge: Instance.GetEdge<EdgeData>;
|
getEdge: Instance.GetEdge<EdgeData>;
|
||||||
toObject: Instance.ToObject<NodeData, EdgeData>;
|
toObject: Instance.ToObject<NodeData, EdgeData>;
|
||||||
deleteElements: Instance.DeleteElements;
|
deleteElements: Instance.DeleteElements;
|
||||||
|
getIntersectingNodes: Instance.GetIntersectingNodes<NodeData>;
|
||||||
|
isNodeIntersecting: Instance.IsNodeIntersecting<NodeData>;
|
||||||
viewportInitialized: boolean;
|
viewportInitialized: boolean;
|
||||||
} & Omit<ViewportHelperFunctions, 'initialized'>;
|
} & Omit<ViewportHelperFunctions, 'initialized'>;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import type { Selection as D3Selection } from 'd3';
|
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';
|
import type { Node, Edge, Connection, EdgeMarkerType, Transform, XYPosition, Rect, NodeInternals } from '../types';
|
||||||
|
|
||||||
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
|
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
|
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
|
||||||
excludeNonSelectableNodes = false
|
excludeNonSelectableNodes = false
|
||||||
): Node[] => {
|
): Node[] => {
|
||||||
const rBox = rectToBox({
|
const paneRect = {
|
||||||
x: (rect.x - tx) / tScale,
|
x: (rect.x - tx) / tScale,
|
||||||
y: (rect.y - ty) / tScale,
|
y: (rect.y - ty) / tScale,
|
||||||
width: rect.width / tScale,
|
width: rect.width / tScale,
|
||||||
height: rect.height / tScale,
|
height: rect.height / tScale,
|
||||||
});
|
};
|
||||||
|
|
||||||
const visibleNodes: Node[] = [];
|
const visibleNodes: Node[] = [];
|
||||||
|
|
||||||
@@ -177,10 +177,8 @@ export const getNodesInside = (
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nBox = rectToBox({ ...positionAbsolute, width: width || 0, height: height || 0 });
|
const nodeRect = { ...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 overlappingArea = getOverlappingArea(paneRect, nodeRect);
|
||||||
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
|
|
||||||
const overlappingArea = Math.ceil(xOverlap * yOverlap);
|
|
||||||
const notInitialized =
|
const notInitialized =
|
||||||
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null;
|
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null;
|
||||||
|
|
||||||
|
|||||||
@@ -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 => ({
|
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
|
||||||
width: node.offsetWidth,
|
width: node.offsetWidth,
|
||||||
@@ -36,9 +36,25 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
|||||||
height: y2 - y,
|
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 =>
|
export const getBoundsOfRects = (rect1: Rect, rect2: Rect): Rect =>
|
||||||
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
|
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 */
|
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||||
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
|
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user