streamlined connections

This commit is contained in:
peterkogo
2024-06-26 15:03:12 +02:00
parent 44254648c8
commit 5493f045ff
25 changed files with 355 additions and 474 deletions
@@ -1,140 +1,17 @@
import { CSSProperties, useCallback } from 'react';
import { CSSProperties } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import {
Position,
ConnectionLineType,
ConnectionMode,
getBezierPath,
getSmoothStepPath,
type HandleType,
getConnectionStatus,
getStraightPath,
} from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import type { ConnectionLineComponent, ReactFlowState, ReactFlowStore } from '../../types';
function getConnectionStatus(isValid: boolean | null) {
if (isValid === null) {
return null;
}
return isValid ? 'valid' : 'invalid';
}
type ConnectionLineProps = {
nodeId: string;
handleType: HandleType;
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
isValid: boolean | null;
};
const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top,
};
const ConnectionLine = ({
nodeId,
handleType,
style,
type = ConnectionLineType.Bezier,
CustomComponent,
isValid,
}: ConnectionLineProps) => {
const { fromNode, startHandle, endHandle, toX, toY, connectionMode } = useStore(
useCallback(
(s: ReactFlowStore) => ({
fromNode: s.nodeLookup.get(nodeId),
startHandle: s.connection.fromHandle,
endHandle: s.connection.toHandle,
toX: (s.connection.position.x - s.transform[0]) / s.transform[2],
toY: (s.connection.position.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode,
}),
[nodeId]
),
shallow
);
const fromHandleBounds = fromNode?.internals.handleBounds;
let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) {
handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source'];
}
if (!fromNode || !handleBounds) {
return null;
}
const handleId = startHandle?.handleId;
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0;
const fromX = fromNode.internals.positionAbsolute.x + fromHandleX;
const fromY = fromNode.internals.positionAbsolute.y + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition =
isValid && endHandle?.position ? endHandle.position : fromPosition ? oppositePosition[fromPosition] : null;
if (!fromPosition || !toPosition) {
return null;
}
if (CustomComponent) {
return (
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode.internals.userNode}
fromHandle={fromHandle}
fromX={fromX}
fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
connectionStatus={getConnectionStatus(isValid)}
/>
);
}
let dAttr = '';
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetPosition: toPosition,
};
if (type === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[dAttr] = getBezierPath(pathParams);
} else if (type === ConnectionLineType.Step) {
[dAttr] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
} else if (type === ConnectionLineType.SmoothStep) {
[dAttr] = getSmoothStepPath(pathParams);
} else if (type === ConnectionLineType.SimpleBezier) {
[dAttr] = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`;
}
return <path d={dAttr} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';
import type { ConnectionLineComponent, ReactFlowState } from '../../types';
type ConnectionLineWrapperProps = {
type: ConnectionLineType;
@@ -144,19 +21,18 @@ type ConnectionLineWrapperProps = {
};
const selector = (s: ReactFlowState) => ({
nodeId: s.connection.fromHandle?.nodeId,
handleType: s.connection.fromHandle?.type,
nodesConnectable: s.nodesConnectable,
isValid: s.connection.isValid,
inProgress: s.connection.inProgress,
width: s.width,
height: s.height,
});
export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
const { nodeId, handleType, nodesConnectable, width, height, isValid } = useStore(selector, shallow);
const isIncorrect = !(nodeId && handleType && width && nodesConnectable);
const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
const renderConnection = !!(width && nodesConnectable && inProgress);
if (isIncorrect) {
if (!renderConnection) {
return null;
}
@@ -168,15 +44,81 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
className="react-flow__connectionline react-flow__container"
>
<g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
<ConnectionLine
nodeId={nodeId}
handleType={handleType}
style={style}
type={type}
CustomComponent={component}
isValid={isValid}
/>
<ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} />
</g>
</svg>
);
}
type ConnectionLineProps = {
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
isValid: boolean | null;
};
const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, isValid }: ConnectionLineProps) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } = useStore(
(s: ReactFlowState) => s.connection,
shallow
);
if (!inProgress) {
return;
}
if (CustomComponent) {
return (
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={from.x}
fromY={from.y}
toX={to.x}
toY={to.y}
fromPosition={fromPosition}
toPosition={toPosition}
connectionStatus={getConnectionStatus(isValid)}
toNode={toNode}
toHandle={toHandle}
/>
);
}
let path = '';
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition,
};
switch (type) {
case ConnectionLineType.Bezier:
[path] = getBezierPath(pathParams);
break;
case ConnectionLineType.SimpleBezier:
[path] = getSimpleBezierPath(pathParams);
break;
case ConnectionLineType.Step:
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
break;
case ConnectionLineType.SmoothStep:
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
}
return <path d={path} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';