refactor(handle): first check element below then connection radius

This commit is contained in:
moklick
2023-01-30 11:39:19 +01:00
parent 079e380a31
commit 75a13e87bc
11 changed files with 348 additions and 19 deletions

View File

@@ -9,6 +9,7 @@ import CustomNode from '../examples/CustomNode';
import DefaultNodes from '../examples/DefaultNodes';
import DragHandle from '../examples/DragHandle';
import DragNDrop from '../examples/DragNDrop';
import EasyConnect from '../examples/EasyConnect';
import Edges from '../examples/Edges';
import EdgeRenderer from '../examples/EdgeRenderer';
import EdgeTypes from '../examples/EdgeTypes';
@@ -96,6 +97,11 @@ const routes: IRoute[] = [
path: '/dragndrop',
component: DragNDrop,
},
{
name: 'EasyConnect',
path: '/easy-connect',
component: EasyConnect,
},
{
name: 'Edges',
path: '/edges',

View File

@@ -0,0 +1,19 @@
import { ConnectionLineComponentProps, getStraightPath } from 'reactflow';
function CustomConnectionLine({ fromX, fromY, toX, toY, connectionLineStyle }: ConnectionLineComponentProps) {
const [edgePath] = getStraightPath({
sourceX: fromX,
sourceY: fromY,
targetX: toX,
targetY: toY,
});
return (
<g>
<path style={connectionLineStyle} fill="none" d={edgePath} />
<circle cx={toX} cy={toY} fill="black" r={3} stroke="black" strokeWidth={1.5} />
</g>
);
}
export default CustomConnectionLine;

View File

@@ -0,0 +1,27 @@
import { Handle, NodeProps, Position, ReactFlowState, useStore } from 'reactflow';
const connectionNodeIdSelector = (state: ReactFlowState) => state.connectionNodeId;
export default function CustomNode({ id }: NodeProps) {
const connectionNodeId = useStore(connectionNodeIdSelector);
const isTarget = connectionNodeId && connectionNodeId !== id;
const targetHandleStyle = { zIndex: isTarget ? 3 : 1 };
const label = isTarget ? 'Drop here' : 'Drag to connect';
return (
<div className="customNode">
<div
className="customNodeBody"
style={{
borderStyle: isTarget ? 'dashed' : 'solid',
backgroundColor: isTarget ? '#ffcce3' : '#ccd9f6',
}}
>
<Handle className="targetHandle" style={{ zIndex: 2 }} position={Position.Right} type="source" />
<Handle className="targetHandle" style={targetHandleStyle} position={Position.Left} type="target" />
{label}
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import { useCallback } from 'react';
import { useStore, getStraightPath, EdgeProps } from 'reactflow';
import { getEdgeParams } from './utils.js';
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
const sourceNode = useStore(useCallback((store) => store.nodeInternals.get(source), [source]));
const targetNode = useStore(useCallback((store) => store.nodeInternals.get(target), [target]));
if (!sourceNode || !targetNode) {
return null;
}
const { sx, sy, tx, ty } = getEdgeParams(sourceNode, targetNode);
const [edgePath] = getStraightPath({
sourceX: sx,
sourceY: sy,
targetX: tx,
targetY: ty,
});
return <path id={id} className="react-flow__edge-path" d={edgePath} markerEnd={markerEnd} style={style} />;
}
export default FloatingEdge;

View File

@@ -0,0 +1,85 @@
import { useCallback } from 'react';
import ReactFlow, { Node, Edge, addEdge, useNodesState, useEdgesState, MarkerType, OnConnect } from 'reactflow';
import CustomNode from './CustomNode';
import FloatingEdge from './FloatingEdge';
import CustomConnectionLine from './CustomConnectionLine';
import 'reactflow/dist/style.css';
import './style.css';
const initialNodes: Node[] = [
{
id: '1',
type: 'custom',
position: { x: 0, y: 0 },
data: {},
},
{
id: '2',
type: 'custom',
position: { x: 250, y: 320 },
data: {},
},
{
id: '3',
type: 'custom',
position: { x: 40, y: 300 },
data: {},
},
{
id: '4',
type: 'custom',
position: { x: 300, y: 0 },
data: {},
},
];
const initialEdges: Edge[] = [];
const connectionLineStyle = {
strokeWidth: 3,
stroke: 'black',
};
const nodeTypes = {
custom: CustomNode,
};
const edgeTypes = {
floating: FloatingEdge,
};
const defaultEdgeOptions = {
style: { strokeWidth: 3, stroke: 'black' },
type: 'floating',
markerEnd: {
type: MarkerType.ArrowClosed,
color: 'black',
},
};
const EasyConnectExample = () => {
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect: OnConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
defaultEdgeOptions={defaultEdgeOptions}
connectionLineComponent={CustomConnectionLine}
connectionLineStyle={connectionLineStyle}
/>
);
};
export default EasyConnectExample;

View File

@@ -0,0 +1,54 @@
.customNodeBody {
width: 150px;
height: 80px;
border: 3px solid black;
position: relative;
overflow: hidden;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
font-weight: bold;
}
.customNode:before {
content: '';
position: absolute;
top: -10px;
left: 50%;
height: 20px;
width: 40px;
transform: translate(-50%, 0);
background: #d6d5e6;
z-index: 1000;
line-height: 1;
border-radius: 4px;
color: #fff;
font-size: 9px;
border: 2px solid #222138;
}
div.sourceHandle {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
border-radius: 0;
transform: none;
border: none;
opacity: 0;
}
div.targetHandle {
width: 100%;
height: 100%;
background: blue;
position: absolute;
top: 0;
left: 0;
border-radius: 0;
transform: none;
border: none;
opacity: 0;
}

View File

@@ -0,0 +1,102 @@
import { Node, Position, MarkerType, XYPosition } from 'reactflow';
// 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) {
// 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,
positionAbsolute: intersectionNodePosition,
} = intersectionNode;
const targetPosition = targetNode.positionAbsolute!;
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.positionAbsolute, ...node };
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 createNodesAndEdges() {
const nodes = [];
const edges = [];
const center = { x: window.innerWidth / 2, y: window.innerHeight / 2 };
nodes.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;
nodes.push({ id: `${i}`, data: { label: 'Source' }, position: { x, y } });
edges.push({
id: `edge-${i}`,
target: 'target',
source: `${i}`,
type: 'floating',
markerEnd: {
type: MarkerType.Arrow,
},
});
}
return { nodes, edges };
}

View File

@@ -1,16 +1,17 @@
import React, { FC, useCallback, useState } from 'react';
import { FC, useCallback, useState } from 'react';
import ReactFlow, {
addEdge,
Handle,
Connection,
Position,
Node,
Edge,
NodeProps,
NodeTypes,
useNodesState,
useEdgesState,
OnConnectStartParams,
OnConnectStart,
OnConnectEnd,
OnConnect,
} from 'reactflow';
import styles from './validation.module.css';
@@ -49,24 +50,24 @@ const ValidationFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const onConnectStart = useCallback(
(event: React.MouseEvent, params: OnConnectStartParams) => {
const onConnectStart: OnConnectStart = useCallback(
(event, params) => {
console.log('on connect start', params, event, value);
setValue(1);
},
[value]
);
const onConnect = useCallback(
(params: Connection | Edge) => {
const onConnect: OnConnect = useCallback(
(params) => {
console.log('on connect', params);
setEdges((eds) => addEdge(params, eds));
},
[setEdges]
);
const onConnectEnd = useCallback(
(event: MouseEvent) => {
const onConnectEnd: OnConnectEnd = useCallback(
(event) => {
console.log('on connect end', event, value);
setValue(0);
},