diff --git a/example/package-lock.json b/example/package-lock.json index 3560909a..520b9afe 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -27,7 +27,7 @@ }, "..": { "name": "react-flow-renderer", - "version": "9.6.7", + "version": "9.6.8", "license": "MIT", "dependencies": { "@babel/runtime": "^7.15.4", diff --git a/example/src/FloatingEdges/FloatingConnectionLine.tsx b/example/src/FloatingEdges/FloatingConnectionLine.tsx new file mode 100644 index 00000000..987af43d --- /dev/null +++ b/example/src/FloatingEdges/FloatingConnectionLine.tsx @@ -0,0 +1,40 @@ +import { FC } from 'react'; +import { getBezierPath, ConnectionLineComponentProps, Node } from 'react-flow-renderer'; + +import { getEdgeParams } from './utils'; + +const FloatingConnectionLine: FC = ({ + targetX, + targetY, + sourcePosition, + targetPosition, + sourceNode, +}) => { + if (!sourceNode) { + return null; + } + + const targetNode = { + id: 'connection-target', + __rf: { width: 1, height: 1, position: { x: targetX, y: targetY } }, + } as Node; + + const { sx, sy } = getEdgeParams(sourceNode, targetNode); + const d = getBezierPath({ + sourceX: sx, + sourceY: sy, + sourcePosition, + targetPosition, + targetX, + targetY, + }); + + return ( + + + + + ); +}; + +export default FloatingConnectionLine; diff --git a/example/src/FloatingEdges/FloatingEdge.tsx b/example/src/FloatingEdges/FloatingEdge.tsx new file mode 100644 index 00000000..60ee4b93 --- /dev/null +++ b/example/src/FloatingEdges/FloatingEdge.tsx @@ -0,0 +1,35 @@ +import { FC, useMemo, CSSProperties } from 'react'; +import { EdgeProps, getMarkerEnd, useStoreState, getBezierPath } from 'react-flow-renderer'; + +import { getEdgeParams } from './utils'; + +const FloatingEdge: FC = ({ id, source, target, arrowHeadType, markerEndId, style }) => { + const nodes = useStoreState((state) => state.nodes); + const markerEnd = getMarkerEnd(arrowHeadType, markerEndId); + + const sourceNode = useMemo(() => nodes.find((n) => n.id === source), [source, nodes]); + const targetNode = useMemo(() => nodes.find((n) => n.id === target), [target, nodes]); + + if (!sourceNode || !targetNode) { + return null; + } + + const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode); + + const d = getBezierPath({ + sourceX: sx, + sourceY: sy, + sourcePosition: sourcePos, + targetPosition: targetPos, + targetX: tx, + targetY: ty, + }); + + return ( + + + + ); +}; + +export default FloatingEdge; diff --git a/example/src/FloatingEdges/index.tsx b/example/src/FloatingEdges/index.tsx new file mode 100644 index 00000000..7bdfc7a2 --- /dev/null +++ b/example/src/FloatingEdges/index.tsx @@ -0,0 +1,53 @@ +import React, { useState } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + Background, + OnLoadParams, + EdgeTypesType, + Elements, + Connection, + Edge, + ArrowHeadType, +} from 'react-flow-renderer'; + +import './style.css'; + +import FloatingEdge from './FloatingEdge'; +import FloatingConnectionLine from './FloatingConnectionLine'; +import { createElements } from './utils'; + +const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); + +const initialElements: Elements = createElements(); + +const edgeTypes: EdgeTypesType = { + floating: FloatingEdge, +}; + +const NodeAsHandleFlow = () => { + const [elements, setElements] = useState(initialElements); + + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + + const onConnect = (params: Connection | Edge) => + setElements((els) => addEdge({ ...params, type: 'floating', arrowHeadType: ArrowHeadType.Arrow }, els)); + + return ( +
+ + + +
+ ); +}; + +export default NodeAsHandleFlow; diff --git a/example/src/FloatingEdges/style.css b/example/src/FloatingEdges/style.css new file mode 100644 index 00000000..052fedb8 --- /dev/null +++ b/example/src/FloatingEdges/style.css @@ -0,0 +1,9 @@ +.floatingedges { + flex-direction: column; + display: flex; + height: 100%; +} + +.floatingedges .react-flow__handle { + opacity: 0; +} diff --git a/example/src/FloatingEdges/utils.ts b/example/src/FloatingEdges/utils.ts new file mode 100644 index 00000000..c8962b72 --- /dev/null +++ b/example/src/FloatingEdges/utils.ts @@ -0,0 +1,99 @@ +import { Position, ArrowHeadType, Node, XYPosition } from 'react-flow-renderer'; + +// this helper function returns the intersection point +// of the line between the center of the intersectionNode and the target node +function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition { + // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a + const { + width: intersectionNodeWidth, + height: intersectionNodeHeight, + position: intersectionNodePosition, + } = intersectionNode.__rf; + const targetPosition = targetNode.__rf.position; + + const w = intersectionNodeWidth / 2; + const h = intersectionNodeHeight / 2; + + const x2 = intersectionNodePosition.x + w; + const y2 = intersectionNodePosition.y + h; + const x1 = targetPosition.x + w; + const y1 = targetPosition.y + h; + + const xx1 = (x1 - x2) / (2 * w) - (y1 - y2) / (2 * h); + const yy1 = (x1 - x2) / (2 * w) + (y1 - y2) / (2 * h); + const a = 1 / (Math.abs(xx1) + Math.abs(yy1)); + const xx3 = a * xx1; + const yy3 = a * yy1; + const x = w * (xx3 + yy3) + x2; + const y = h * (-xx3 + yy3) + y2; + + return { x, y }; +} + +// returns the position (top,right,bottom or right) passed node compared to the intersection point +function getEdgePosition(node: Node, intersectionPoint: XYPosition) { + const n = { ...node.__rf.position, ...node.__rf }; + const nx = Math.round(n.x); + const ny = Math.round(n.y); + const px = Math.round(intersectionPoint.x); + const py = Math.round(intersectionPoint.y); + + if (px <= nx + 1) { + return Position.Left; + } + if (px >= nx + n.width - 1) { + return Position.Right; + } + if (py <= ny + 1) { + return Position.Top; + } + if (py >= n.y + n.height - 1) { + return Position.Bottom; + } + + return Position.Top; +} + +// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge +export function getEdgeParams(source: Node, target: Node) { + const sourceIntersectionPoint = getNodeIntersection(source, target); + const targetIntersectionPoint = getNodeIntersection(target, source); + + const sourcePos = getEdgePosition(source, sourceIntersectionPoint); + const targetPos = getEdgePosition(target, targetIntersectionPoint); + + return { + sx: sourceIntersectionPoint.x, + sy: sourceIntersectionPoint.y, + tx: targetIntersectionPoint.x, + ty: targetIntersectionPoint.y, + sourcePos, + targetPos, + }; +} + +export function createElements() { + const elements = []; + const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; + + elements.push({ id: 'target', data: { label: 'Target' }, position: center }); + + for (let i = 0; i < 8; i++) { + const degrees = i * (360 / 8); + const radians = degrees * (Math.PI / 180); + const x = 250 * Math.cos(radians) + center.x; + const y = 250 * Math.sin(radians) + center.y; + + elements.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } }); + + elements.push({ + id: `edge-${i}`, + target: 'target', + source: `${i}`, + type: 'floating', + arrowHeadType: ArrowHeadType.Arrow, + }); + } + + return elements; +} diff --git a/example/src/index.tsx b/example/src/index.tsx index f81278ba..8371c0af 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -14,6 +14,7 @@ import Provider from './Provider'; import Hidden from './Hidden'; import EdgeTypes from './EdgeTypes'; import CustomConnectionLine from './CustomConnectionLine'; +import FloatingEdges from './FloatingEdges'; import NodeTypeChange from './NodeTypeChange'; import NodeTypesObjectChange from './NodeTypesObjectChange'; import UpdatableEdge from './UpdatableEdge'; @@ -79,6 +80,10 @@ const routes = [ path: '/custom-connectionline', component: CustomConnectionLine, }, + { + path: '/floating-edges', + component: FloatingEdges, + }, { path: '/nodetype-change', component: NodeTypeChange,