From b36c713650323d542aa206c22f3d3ee8d8f1a3d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 14 Oct 2021 09:15:04 +0200 Subject: [PATCH 1/4] feat(example): add example with floating edge and whole node as handle --- example/src/NodeAsHandle/CustomEdge.tsx | 163 ++++++++++++++++++++++++ example/src/NodeAsHandle/CustomNode.tsx | 16 +++ example/src/NodeAsHandle/index.tsx | 92 +++++++++++++ example/src/NodeAsHandle/style.css | 24 ++++ example/src/index.tsx | 5 + 5 files changed, 300 insertions(+) create mode 100644 example/src/NodeAsHandle/CustomEdge.tsx create mode 100644 example/src/NodeAsHandle/CustomNode.tsx create mode 100644 example/src/NodeAsHandle/index.tsx create mode 100644 example/src/NodeAsHandle/style.css diff --git a/example/src/NodeAsHandle/CustomEdge.tsx b/example/src/NodeAsHandle/CustomEdge.tsx new file mode 100644 index 00000000..032b9f36 --- /dev/null +++ b/example/src/NodeAsHandle/CustomEdge.tsx @@ -0,0 +1,163 @@ +import { FC, useMemo } from 'react'; +import cc from 'classcat'; +import { + EdgeProps, + getMarkerEnd, + useStoreState, + Position, + getBezierPath, + ConnectionLineComponentProps, +} from 'react-flow-renderer'; + +function getNodeIntersection(source: any, target: any) { + // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a + const w = source.__rf.width / 2; + const h = source.__rf.height / 2; + + const x2 = source.__rf.position.x + w; + const y2 = source.__rf.position.y + h; + const x1 = target.__rf.position.x; + const y1 = target.__rf.position.y; + + 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]; +} + +function getEdgePosition(node: any, x: number, y: number) { + const n = { ...node.__rf.position, ...node.__rf }; + const nx = Math.round(n.x); + const ny = Math.round(n.y); + const px = Math.round(x); + const py = Math.round(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; +} + +function getArrow(source: any, target: any) { + const [sx, sy] = getNodeIntersection(source, target); + const [tx, ty] = getNodeIntersection(target, source); + + const sourcePos = getEdgePosition(source, sx, sy); + const targetPos = getEdgePosition(target, tx, ty); + + return { + sx, + sy, + tx, + ty, + label: null, + sourcePos, + targetPos, + }; +} + +const CustomEdge: FC = ({ + id, + source, + target, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + data, + 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, label, sourcePos, targetPos } = getArrow(sourceNode, targetNode); + + const d = getBezierPath({ + sourceX: sx, + sourceY: sy, + sourcePosition: sourcePos, + targetPosition: targetPos, + targetX: tx, + targetY: ty, + }); + + return ( + <> + + + + {label && ( + + + {label} + + + )} + + ); +}; + +export const ConnectionLine: FC = ({ + sourceX, + sourceY, + targetX, + targetY, + sourceNode, +}) => { + if (!sourceNode) { + return null; + } + + const targetNode = { __rf: { width: 100, height: 100, position: { x: targetX, y: targetY - 50 } } }; + const { sx, sy, tx, ty, label, sourcePos, targetPos } = getArrow(sourceNode, targetNode); + const d = getBezierPath({ + sourceX: sx, + sourceY: sy, + sourcePosition: sourcePos, + targetPosition: targetPos, + targetX: tx, + targetY: ty, + }); + + return ( + + + + + ); +}; + +export default CustomEdge; diff --git a/example/src/NodeAsHandle/CustomNode.tsx b/example/src/NodeAsHandle/CustomNode.tsx new file mode 100644 index 00000000..c1bc00de --- /dev/null +++ b/example/src/NodeAsHandle/CustomNode.tsx @@ -0,0 +1,16 @@ +import { Handle, Position, NodeProps } from 'react-flow-renderer'; + +const CustomNode = ({ data }: NodeProps) => { + const handleType = data?.isTarget ? 'target' : 'source'; + + return ( +
+ This is a node +
+ +
+
+ ); +}; + +export default CustomNode; diff --git a/example/src/NodeAsHandle/index.tsx b/example/src/NodeAsHandle/index.tsx new file mode 100644 index 00000000..b24b0571 --- /dev/null +++ b/example/src/NodeAsHandle/index.tsx @@ -0,0 +1,92 @@ +import React, { useState } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + MiniMap, + Controls, + Background, + OnLoadParams, + EdgeTypesType, + Elements, + Connection, + Edge, + ArrowHeadType, + NodeTypesType, +} from 'react-flow-renderer'; + +import CustomEdge, { ConnectionLine } from './CustomEdge'; +import CustomNode from './CustomNode'; + +import './style.css'; + +const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); + +const getCustomEdge = (props: any) => ({ + type: 'custom', + arrowHeadType: ArrowHeadType.Arrow, + ...props, +}); + +function createElements() { + const elements = []; + const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; + + elements.push({ type: 'custom', id: 'source', data: { label: 'Source', isTarget: false }, 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({ type: 'custom', id: `${i}`, data: { label: 'Target', isTarget: true }, position: { x, y } }); + + if (i % 2 === 0) { + elements.push( + getCustomEdge({ + id: `source-${i}`, + source: 'source', + target: `${i}`, + }) + ); + } + } + + return elements; +} + +const initialElements: Elements = createElements(); + +const edgeTypes: EdgeTypesType = { + custom: CustomEdge, +}; + +const nodeTypes: NodeTypesType = { + custom: CustomNode, +}; + +const NodeAsHandleFlow = () => { + const [elements, setElements] = useState(initialElements); + + const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); + const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(getCustomEdge(params), els)); + + return ( + + + + + + ); +}; + +export default NodeAsHandleFlow; diff --git a/example/src/NodeAsHandle/style.css b/example/src/NodeAsHandle/style.css new file mode 100644 index 00000000..65661b0a --- /dev/null +++ b/example/src/NodeAsHandle/style.css @@ -0,0 +1,24 @@ +.customnode { + padding: 10px; + border: 1px solid #ddd; + position: relative; +} + +.handlewrapper { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.react-flow__handle { + background-color: red; + opacity: 0.5; + height: 100%; + width: 100%; + border-radius: 0; + left: 0; + top: 0; + transform: none; +} diff --git a/example/src/index.tsx b/example/src/index.tsx index f81278ba..0f8a4b77 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 NodeAsHandle from './NodeAsHandle'; import NodeTypeChange from './NodeTypeChange'; import NodeTypesObjectChange from './NodeTypesObjectChange'; import UpdatableEdge from './UpdatableEdge'; @@ -79,6 +80,10 @@ const routes = [ path: '/custom-connectionline', component: CustomConnectionLine, }, + { + path: '/node-as-handle', + component: NodeAsHandle, + }, { path: '/nodetype-change', component: NodeTypeChange, From ecc02a1ee9bcbea4776d00764832488dfac6ba0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christopher=20M=C3=B6ller?= Date: Thu, 14 Oct 2021 10:52:46 +0200 Subject: [PATCH 2/4] feat(floatingedge): configure connectionline --- example/src/NodeAsHandle/CustomEdge.tsx | 16 ++++++++-------- example/src/NodeAsHandle/CustomNode.tsx | 16 ---------------- example/src/NodeAsHandle/index.tsx | 13 ++----------- example/src/NodeAsHandle/style.css | 24 ------------------------ 4 files changed, 10 insertions(+), 59 deletions(-) delete mode 100644 example/src/NodeAsHandle/CustomNode.tsx delete mode 100644 example/src/NodeAsHandle/style.css diff --git a/example/src/NodeAsHandle/CustomEdge.tsx b/example/src/NodeAsHandle/CustomEdge.tsx index 032b9f36..988b1751 100644 --- a/example/src/NodeAsHandle/CustomEdge.tsx +++ b/example/src/NodeAsHandle/CustomEdge.tsx @@ -131,25 +131,25 @@ const CustomEdge: FC = ({ }; export const ConnectionLine: FC = ({ - sourceX, - sourceY, targetX, targetY, + sourcePosition, + targetPosition, sourceNode, }) => { if (!sourceNode) { return null; } - const targetNode = { __rf: { width: 100, height: 100, position: { x: targetX, y: targetY - 50 } } }; - const { sx, sy, tx, ty, label, sourcePos, targetPos } = getArrow(sourceNode, targetNode); + const targetNode = { __rf: { width: 1, height: 1, position: { x: targetX, y: targetY } } }; + const { sx, sy } = getArrow(sourceNode, targetNode); const d = getBezierPath({ sourceX: sx, sourceY: sy, - sourcePosition: sourcePos, - targetPosition: targetPos, - targetX: tx, - targetY: ty, + sourcePosition, + targetPosition, + targetX, + targetY, }); return ( diff --git a/example/src/NodeAsHandle/CustomNode.tsx b/example/src/NodeAsHandle/CustomNode.tsx deleted file mode 100644 index c1bc00de..00000000 --- a/example/src/NodeAsHandle/CustomNode.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { Handle, Position, NodeProps } from 'react-flow-renderer'; - -const CustomNode = ({ data }: NodeProps) => { - const handleType = data?.isTarget ? 'target' : 'source'; - - return ( -
- This is a node -
- -
-
- ); -}; - -export default CustomNode; diff --git a/example/src/NodeAsHandle/index.tsx b/example/src/NodeAsHandle/index.tsx index b24b0571..9e3879d3 100644 --- a/example/src/NodeAsHandle/index.tsx +++ b/example/src/NodeAsHandle/index.tsx @@ -12,13 +12,9 @@ import ReactFlow, { Connection, Edge, ArrowHeadType, - NodeTypesType, } from 'react-flow-renderer'; import CustomEdge, { ConnectionLine } from './CustomEdge'; -import CustomNode from './CustomNode'; - -import './style.css'; const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); @@ -32,7 +28,7 @@ function createElements() { const elements = []; const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; - elements.push({ type: 'custom', id: 'source', data: { label: 'Source', isTarget: false }, position: center }); + elements.push({ id: 'source', data: { label: 'Source', isTarget: false }, position: center }); for (let i = 0; i < 8; i++) { const degrees = i * (360 / 8); @@ -40,7 +36,7 @@ function createElements() { const x = 250 * Math.cos(radians) + center.x; const y = 250 * Math.sin(radians) + center.y; - elements.push({ type: 'custom', id: `${i}`, data: { label: 'Target', isTarget: true }, position: { x, y } }); + elements.push({ id: `${i}`, data: { label: 'Target', isTarget: true }, position: { x, y } }); if (i % 2 === 0) { elements.push( @@ -62,10 +58,6 @@ const edgeTypes: EdgeTypesType = { custom: CustomEdge, }; -const nodeTypes: NodeTypesType = { - custom: CustomNode, -}; - const NodeAsHandleFlow = () => { const [elements, setElements] = useState(initialElements); @@ -79,7 +71,6 @@ const NodeAsHandleFlow = () => { onConnect={onConnect} onLoad={onLoad} edgeTypes={edgeTypes} - nodeTypes={nodeTypes} connectionLineComponent={ConnectionLine} > diff --git a/example/src/NodeAsHandle/style.css b/example/src/NodeAsHandle/style.css deleted file mode 100644 index 65661b0a..00000000 --- a/example/src/NodeAsHandle/style.css +++ /dev/null @@ -1,24 +0,0 @@ -.customnode { - padding: 10px; - border: 1px solid #ddd; - position: relative; -} - -.handlewrapper { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} - -.react-flow__handle { - background-color: red; - opacity: 0.5; - height: 100%; - width: 100%; - border-radius: 0; - left: 0; - top: 0; - transform: none; -} From ab2391fd5151c38045eab7a04950ec7115ba78aa Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 14 Oct 2021 11:34:06 +0200 Subject: [PATCH 3/4] refactor(floating-edge-example): cleanup --- example/package-lock.json | 2 +- .../FloatingEdges/FloatingConnectionLine.tsx | 36 ++++ example/src/FloatingEdges/FloatingEdge.tsx | 35 ++++ example/src/FloatingEdges/index.tsx | 52 ++++++ example/src/FloatingEdges/utils.ts | 94 ++++++++++ example/src/NodeAsHandle/CustomEdge.tsx | 163 ------------------ example/src/NodeAsHandle/index.tsx | 83 --------- example/src/index.tsx | 6 +- package-lock.json | 3 + 9 files changed, 224 insertions(+), 250 deletions(-) create mode 100644 example/src/FloatingEdges/FloatingConnectionLine.tsx create mode 100644 example/src/FloatingEdges/FloatingEdge.tsx create mode 100644 example/src/FloatingEdges/index.tsx create mode 100644 example/src/FloatingEdges/utils.ts delete mode 100644 example/src/NodeAsHandle/CustomEdge.tsx delete mode 100644 example/src/NodeAsHandle/index.tsx 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..f45b16dc --- /dev/null +++ b/example/src/FloatingEdges/FloatingConnectionLine.tsx @@ -0,0 +1,36 @@ +import { FC } from 'react'; +import { getBezierPath, ConnectionLineComponentProps } from 'react-flow-renderer'; + +import { getArrow } from './utils'; + +const FloatingConnectionLine: FC = ({ + targetX, + targetY, + sourcePosition, + targetPosition, + sourceNode, +}) => { + if (!sourceNode) { + return null; + } + + const targetNode = { __rf: { width: 1, height: 1, position: { x: targetX, y: targetY } } }; + const { sx, sy } = getArrow(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..e39c318f --- /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 { getArrow } 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 } = getArrow(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..04a5960b --- /dev/null +++ b/example/src/FloatingEdges/index.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; + +import ReactFlow, { + removeElements, + addEdge, + MiniMap, + Controls, + Background, + OnLoadParams, + EdgeTypesType, + Elements, + Connection, + Edge, + ArrowHeadType, +} from 'react-flow-renderer'; + +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/utils.ts b/example/src/FloatingEdges/utils.ts new file mode 100644 index 00000000..352e7c3f --- /dev/null +++ b/example/src/FloatingEdges/utils.ts @@ -0,0 +1,94 @@ +import { Position, ArrowHeadType, Node } from 'react-flow-renderer'; + +function getNodeIntersection(source: Node, target: Node) { + // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a + const { width: sourceWidth, height: sourceHeight, position: sourcePosition } = source.__rf; + const targetPosition = target.__rf.position; + + const w = sourceWidth / 2; + const h = sourceHeight / 2; + + const x2 = sourcePosition.x + w; + const y2 = sourcePosition.y + h; + const x1 = targetPosition.x; + const y1 = targetPosition.y; + + 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]; +} + +function getEdgePosition(node: Node, x: number, y: number) { + const n = { ...node.__rf.position, ...node.__rf }; + const nx = Math.round(n.x); + const ny = Math.round(n.y); + const px = Math.round(x); + const py = Math.round(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; +} + +export function getArrow(source: any, target: any) { + const [sx, sy] = getNodeIntersection(source, target); + const [tx, ty] = getNodeIntersection(target, source); + + const sourcePos = getEdgePosition(source, sx, sy); + const targetPos = getEdgePosition(target, tx, ty); + + return { + sx, + sy, + tx, + ty, + label: null, + sourcePos, + targetPos, + }; +} + +export function createElements() { + const elements = []; + const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; + + elements.push({ id: 'source', data: { label: 'Source', isTarget: false }, 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: 'Target', isTarget: true }, position: { x, y } }); + + if (i % 2 === 0) { + elements.push({ + id: `source-${i}`, + source: 'source', + target: `${i}`, + type: 'floating', + arrowHeadType: ArrowHeadType.Arrow, + }); + } + } + + return elements; +} diff --git a/example/src/NodeAsHandle/CustomEdge.tsx b/example/src/NodeAsHandle/CustomEdge.tsx deleted file mode 100644 index 988b1751..00000000 --- a/example/src/NodeAsHandle/CustomEdge.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { FC, useMemo } from 'react'; -import cc from 'classcat'; -import { - EdgeProps, - getMarkerEnd, - useStoreState, - Position, - getBezierPath, - ConnectionLineComponentProps, -} from 'react-flow-renderer'; - -function getNodeIntersection(source: any, target: any) { - // https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a - const w = source.__rf.width / 2; - const h = source.__rf.height / 2; - - const x2 = source.__rf.position.x + w; - const y2 = source.__rf.position.y + h; - const x1 = target.__rf.position.x; - const y1 = target.__rf.position.y; - - 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]; -} - -function getEdgePosition(node: any, x: number, y: number) { - const n = { ...node.__rf.position, ...node.__rf }; - const nx = Math.round(n.x); - const ny = Math.round(n.y); - const px = Math.round(x); - const py = Math.round(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; -} - -function getArrow(source: any, target: any) { - const [sx, sy] = getNodeIntersection(source, target); - const [tx, ty] = getNodeIntersection(target, source); - - const sourcePos = getEdgePosition(source, sx, sy); - const targetPos = getEdgePosition(target, tx, ty); - - return { - sx, - sy, - tx, - ty, - label: null, - sourcePos, - targetPos, - }; -} - -const CustomEdge: FC = ({ - id, - source, - target, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - data, - 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, label, sourcePos, targetPos } = getArrow(sourceNode, targetNode); - - const d = getBezierPath({ - sourceX: sx, - sourceY: sy, - sourcePosition: sourcePos, - targetPosition: targetPos, - targetX: tx, - targetY: ty, - }); - - return ( - <> - - - - {label && ( - - - {label} - - - )} - - ); -}; - -export const ConnectionLine: FC = ({ - targetX, - targetY, - sourcePosition, - targetPosition, - sourceNode, -}) => { - if (!sourceNode) { - return null; - } - - const targetNode = { __rf: { width: 1, height: 1, position: { x: targetX, y: targetY } } }; - const { sx, sy } = getArrow(sourceNode, targetNode); - const d = getBezierPath({ - sourceX: sx, - sourceY: sy, - sourcePosition, - targetPosition, - targetX, - targetY, - }); - - return ( - - - - - ); -}; - -export default CustomEdge; diff --git a/example/src/NodeAsHandle/index.tsx b/example/src/NodeAsHandle/index.tsx deleted file mode 100644 index 9e3879d3..00000000 --- a/example/src/NodeAsHandle/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import React, { useState } from 'react'; - -import ReactFlow, { - removeElements, - addEdge, - MiniMap, - Controls, - Background, - OnLoadParams, - EdgeTypesType, - Elements, - Connection, - Edge, - ArrowHeadType, -} from 'react-flow-renderer'; - -import CustomEdge, { ConnectionLine } from './CustomEdge'; - -const onLoad = (reactFlowInstance: OnLoadParams) => reactFlowInstance.fitView(); - -const getCustomEdge = (props: any) => ({ - type: 'custom', - arrowHeadType: ArrowHeadType.Arrow, - ...props, -}); - -function createElements() { - const elements = []; - const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; - - elements.push({ id: 'source', data: { label: 'Source', isTarget: false }, 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: 'Target', isTarget: true }, position: { x, y } }); - - if (i % 2 === 0) { - elements.push( - getCustomEdge({ - id: `source-${i}`, - source: 'source', - target: `${i}`, - }) - ); - } - } - - return elements; -} - -const initialElements: Elements = createElements(); - -const edgeTypes: EdgeTypesType = { - custom: CustomEdge, -}; - -const NodeAsHandleFlow = () => { - const [elements, setElements] = useState(initialElements); - - const onElementsRemove = (elementsToRemove: Elements) => setElements((els) => removeElements(elementsToRemove, els)); - const onConnect = (params: Connection | Edge) => setElements((els) => addEdge(getCustomEdge(params), els)); - - return ( - - - - - - ); -}; - -export default NodeAsHandleFlow; diff --git a/example/src/index.tsx b/example/src/index.tsx index 0f8a4b77..8371c0af 100644 --- a/example/src/index.tsx +++ b/example/src/index.tsx @@ -14,7 +14,7 @@ import Provider from './Provider'; import Hidden from './Hidden'; import EdgeTypes from './EdgeTypes'; import CustomConnectionLine from './CustomConnectionLine'; -import NodeAsHandle from './NodeAsHandle'; +import FloatingEdges from './FloatingEdges'; import NodeTypeChange from './NodeTypeChange'; import NodeTypesObjectChange from './NodeTypesObjectChange'; import UpdatableEdge from './UpdatableEdge'; @@ -81,8 +81,8 @@ const routes = [ component: CustomConnectionLine, }, { - path: '/node-as-handle', - component: NodeAsHandle, + path: '/floating-edges', + component: FloatingEdges, }, { path: '/nodetype-change', diff --git a/package-lock.json b/package-lock.json index ace876fc..66ed96b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,9 @@ "start-server-and-test": "^1.14.0", "typescript": "^4.4.3" }, + "engines": { + "node": ">=12" + }, "peerDependencies": { "react": "16 || 17", "react-dom": "16 || 17" From ceb96201deef186718e9e79f0afc80261aabe9f0 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 14 Oct 2021 12:29:05 +0200 Subject: [PATCH 4/4] refactor(floating-edges-example): comments and cleanup --- .../FloatingEdges/FloatingConnectionLine.tsx | 12 ++- example/src/FloatingEdges/FloatingEdge.tsx | 4 +- example/src/FloatingEdges/index.tsx | 29 +++---- example/src/FloatingEdges/style.css | 9 +++ example/src/FloatingEdges/utils.ts | 75 ++++++++++--------- 5 files changed, 74 insertions(+), 55 deletions(-) create mode 100644 example/src/FloatingEdges/style.css diff --git a/example/src/FloatingEdges/FloatingConnectionLine.tsx b/example/src/FloatingEdges/FloatingConnectionLine.tsx index f45b16dc..987af43d 100644 --- a/example/src/FloatingEdges/FloatingConnectionLine.tsx +++ b/example/src/FloatingEdges/FloatingConnectionLine.tsx @@ -1,7 +1,7 @@ import { FC } from 'react'; -import { getBezierPath, ConnectionLineComponentProps } from 'react-flow-renderer'; +import { getBezierPath, ConnectionLineComponentProps, Node } from 'react-flow-renderer'; -import { getArrow } from './utils'; +import { getEdgeParams } from './utils'; const FloatingConnectionLine: FC = ({ targetX, @@ -14,8 +14,12 @@ const FloatingConnectionLine: FC = ({ return null; } - const targetNode = { __rf: { width: 1, height: 1, position: { x: targetX, y: targetY } } }; - const { sx, sy } = getArrow(sourceNode, targetNode); + 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, diff --git a/example/src/FloatingEdges/FloatingEdge.tsx b/example/src/FloatingEdges/FloatingEdge.tsx index e39c318f..60ee4b93 100644 --- a/example/src/FloatingEdges/FloatingEdge.tsx +++ b/example/src/FloatingEdges/FloatingEdge.tsx @@ -1,7 +1,7 @@ import { FC, useMemo, CSSProperties } from 'react'; import { EdgeProps, getMarkerEnd, useStoreState, getBezierPath } from 'react-flow-renderer'; -import { getArrow } from './utils'; +import { getEdgeParams } from './utils'; const FloatingEdge: FC = ({ id, source, target, arrowHeadType, markerEndId, style }) => { const nodes = useStoreState((state) => state.nodes); @@ -14,7 +14,7 @@ const FloatingEdge: FC = ({ id, source, target, arrowHeadType, marker return null; } - const { sx, sy, tx, ty, sourcePos, targetPos } = getArrow(sourceNode, targetNode); + const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode); const d = getBezierPath({ sourceX: sx, diff --git a/example/src/FloatingEdges/index.tsx b/example/src/FloatingEdges/index.tsx index 04a5960b..7bdfc7a2 100644 --- a/example/src/FloatingEdges/index.tsx +++ b/example/src/FloatingEdges/index.tsx @@ -3,8 +3,6 @@ import React, { useState } from 'react'; import ReactFlow, { removeElements, addEdge, - MiniMap, - Controls, Background, OnLoadParams, EdgeTypesType, @@ -14,6 +12,8 @@ import ReactFlow, { ArrowHeadType, } from 'react-flow-renderer'; +import './style.css'; + import FloatingEdge from './FloatingEdge'; import FloatingConnectionLine from './FloatingConnectionLine'; import { createElements } from './utils'; @@ -30,22 +30,23 @@ 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 ( - - - - - +
+ + + +
); }; 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 index 352e7c3f..c8962b72 100644 --- a/example/src/FloatingEdges/utils.ts +++ b/example/src/FloatingEdges/utils.ts @@ -1,17 +1,23 @@ -import { Position, ArrowHeadType, Node } from 'react-flow-renderer'; +import { Position, ArrowHeadType, Node, XYPosition } from 'react-flow-renderer'; -function getNodeIntersection(source: Node, target: Node) { +// 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: sourceWidth, height: sourceHeight, position: sourcePosition } = source.__rf; - const targetPosition = target.__rf.position; + const { + width: intersectionNodeWidth, + height: intersectionNodeHeight, + position: intersectionNodePosition, + } = intersectionNode.__rf; + const targetPosition = targetNode.__rf.position; - const w = sourceWidth / 2; - const h = sourceHeight / 2; + const w = intersectionNodeWidth / 2; + const h = intersectionNodeHeight / 2; - const x2 = sourcePosition.x + w; - const y2 = sourcePosition.y + h; - const x1 = targetPosition.x; - const y1 = targetPosition.y; + 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); @@ -21,15 +27,16 @@ function getNodeIntersection(source: Node, target: Node) { const x = w * (xx3 + yy3) + x2; const y = h * (-xx3 + yy3) + y2; - return [x, y]; + return { x, y }; } -function getEdgePosition(node: Node, x: number, y: number) { +// 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(x); - const py = Math.round(y); + const px = Math.round(intersectionPoint.x); + const py = Math.round(intersectionPoint.y); if (px <= nx + 1) { return Position.Left; @@ -47,19 +54,19 @@ function getEdgePosition(node: Node, x: number, y: number) { return Position.Top; } -export function getArrow(source: any, target: any) { - const [sx, sy] = getNodeIntersection(source, target); - const [tx, ty] = getNodeIntersection(target, source); +// 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, sx, sy); - const targetPos = getEdgePosition(target, tx, ty); + const sourcePos = getEdgePosition(source, sourceIntersectionPoint); + const targetPos = getEdgePosition(target, targetIntersectionPoint); return { - sx, - sy, - tx, - ty, - label: null, + sx: sourceIntersectionPoint.x, + sy: sourceIntersectionPoint.y, + tx: targetIntersectionPoint.x, + ty: targetIntersectionPoint.y, sourcePos, targetPos, }; @@ -69,7 +76,7 @@ export function createElements() { const elements = []; const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 }; - elements.push({ id: 'source', data: { label: 'Source', isTarget: false }, position: center }); + elements.push({ id: 'target', data: { label: 'Target' }, position: center }); for (let i = 0; i < 8; i++) { const degrees = i * (360 / 8); @@ -77,17 +84,15 @@ export function createElements() { const x = 250 * Math.cos(radians) + center.x; const y = 250 * Math.sin(radians) + center.y; - elements.push({ id: `${i}`, data: { label: 'Target', isTarget: true }, position: { x, y } }); + elements.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } }); - if (i % 2 === 0) { - elements.push({ - id: `source-${i}`, - source: 'source', - target: `${i}`, - type: 'floating', - arrowHeadType: ArrowHeadType.Arrow, - }); - } + elements.push({ + id: `edge-${i}`, + target: 'target', + source: `${i}`, + type: 'floating', + arrowHeadType: ArrowHeadType.Arrow, + }); } return elements;