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] 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,