Merge pull request #1984 from Sec-ant/refactor-bezier-function

refactor Bezier function
This commit is contained in:
Moritz Klack
2022-03-23 10:31:40 +01:00
committed by GitHub
4 changed files with 323 additions and 122 deletions
+69 -36
View File
@@ -4,15 +4,7 @@ import shallow from 'zustand/shallow';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { getBezierPath } from '../Edges/BezierEdge'; import { getBezierPath } from '../Edges/BezierEdge';
import { getSmoothStepPath } from '../Edges/SmoothStepEdge'; import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
import { import { ConnectionLineType, ConnectionLineComponent, HandleType, Node, ReactFlowState, Position } from '../../types';
HandleElement,
ConnectionLineType,
ConnectionLineComponent,
HandleType,
Node,
ReactFlowState,
Position,
} from '../../types';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
interface ConnectionLineProps { interface ConnectionLineProps {
@@ -29,13 +21,6 @@ interface ConnectionLineProps {
const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform }); const selector = (s: ReactFlowState) => ({ nodeInternals: s.nodeInternals, transform: s.transform });
const getSourceHandle = (handleId: string | null, sourceNode: Node, connectionHandleType: HandleType) => {
const handleTypeInverted = connectionHandleType === 'source' ? 'target' : 'source';
const handleBound = sourceNode.handleBounds?.[connectionHandleType] || sourceNode.handleBounds?.[handleTypeInverted];
return handleId ? handleBound?.find((d: HandleElement) => d.id === handleId) : handleBound?.[0];
};
export default ({ export default ({
connectionNodeId, connectionNodeId,
connectionHandleId, connectionHandleId,
@@ -51,28 +36,74 @@ export default ({
const handleId = connectionHandleId; const handleId = connectionHandleId;
const { nodeInternals, transform } = useStore(selector, shallow); const { nodeInternals, transform } = useStore(selector, shallow);
const sourceNode = useRef<Node | undefined>(nodeInternals.get(nodeId)); const fromNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
if ( if (
!sourceNode.current || !fromNode.current ||
!sourceNode.current || !fromNode.current ||
!isConnectable || !isConnectable ||
!sourceNode.current.handleBounds?.[connectionHandleType] !fromNode.current.handleBounds?.[connectionHandleType]
) { ) {
return null; return null;
} }
const sourceHandle = getSourceHandle(handleId, sourceNode.current, connectionHandleType); const handleBound = fromNode.current.handleBounds?.[connectionHandleType];
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : (sourceNode.current?.width ?? 0) / 2; const fromHandle = handleId ? handleBound?.find((d) => d.id === handleId) : handleBound?.[0];
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.current?.height ?? 0; const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.current?.width ?? 0) / 2;
const sourceX = (sourceNode.current.positionAbsolute?.x || 0) + sourceHandleX; const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.current?.height ?? 0;
const sourceY = (sourceNode.current.positionAbsolute?.y || 0) + sourceHandleY; const fromX = (fromNode.current.positionAbsolute?.x || 0) + fromHandleX;
const fromY = (fromNode.current.positionAbsolute?.y || 0) + fromHandleY;
const targetX = (connectionPositionX - transform[0]) / transform[2]; const toX = (connectionPositionX - transform[0]) / transform[2];
const targetY = (connectionPositionY - transform[1]) / transform[2]; const toY = (connectionPositionY - transform[1]) / transform[2];
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right; const fromPosition = fromHandle?.position;
const targetPosition = isRightOrLeft ? Position.Left : Position.Top;
let toPosition: Position | undefined;
switch (fromPosition) {
case Position.Left:
toPosition = Position.Right;
break;
case Position.Right:
toPosition = Position.Left;
break;
case Position.Top:
toPosition = Position.Bottom;
break;
case Position.Bottom:
toPosition = Position.Top;
break;
}
let sourceX: number,
sourceY: number,
sourcePosition: Position | undefined,
targetX: number,
targetY: number,
targetPosition: Position | undefined;
switch (connectionHandleType) {
case 'source':
{
sourceX = fromX;
sourceY = fromY;
sourcePosition = fromPosition;
targetX = toX;
targetY = toY;
targetPosition = toPosition;
}
break;
case 'target':
{
sourceX = toX;
sourceY = toY;
sourcePosition = toPosition;
targetX = fromX;
targetY = fromY;
targetPosition = fromPosition;
}
break;
}
if (CustomConnectionLineComponent) { if (CustomConnectionLineComponent) {
return ( return (
@@ -80,14 +111,17 @@ export default ({
<CustomConnectionLineComponent <CustomConnectionLineComponent
sourceX={sourceX} sourceX={sourceX}
sourceY={sourceY} sourceY={sourceY}
sourcePosition={sourceHandle?.position} sourcePosition={sourcePosition}
targetX={targetX} targetX={targetX}
targetY={targetY} targetY={targetY}
targetPosition={targetPosition} targetPosition={targetPosition}
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
sourceNode={sourceNode.current as Node} fromNode={fromNode.current}
sourceHandle={sourceHandle} fromHandle={fromHandle}
// backward compatibility, mark as deprecated?
sourceNode={fromNode.current}
sourceHandle={fromHandle}
/> />
</g> </g>
); );
@@ -98,16 +132,15 @@ export default ({
const pathParams = { const pathParams = {
sourceX, sourceX,
sourceY, sourceY,
sourcePosition: sourceHandle?.position, sourcePosition,
targetX, targetX,
targetY, targetY,
targetPosition, targetPosition,
}; };
if (connectionLineType === ConnectionLineType.Bezier) { if (connectionLineType === ConnectionLineType.Bezier) {
// @TODO: we need another getBezier function, that handles a connection line. // we assume the destination position is opposite to the source position
// Since we don't know the target position, we can't use the default bezier function here. dAttr = getBezierPath(pathParams);
dAttr = getBezierPath({ ...pathParams, curvature: 0 });
} else if (connectionLineType === ConnectionLineType.Step) { } else if (connectionLineType === ConnectionLineType.Step) {
dAttr = getSmoothStepPath({ dAttr = getSmoothStepPath({
...pathParams, ...pathParams,
+106 -78
View File
@@ -1,7 +1,6 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { EdgeProps, Position } from '../../types'; import { EdgeProps, Position } from '../../types';
import BaseEdge from './BaseEdge'; import BaseEdge from './BaseEdge';
import { getCenter } from './utils';
export interface GetBezierPathParams { export interface GetBezierPathParams {
sourceX: number; sourceX: number;
@@ -11,12 +10,56 @@ export interface GetBezierPathParams {
targetY: number; targetY: number;
targetPosition?: Position; targetPosition?: Position;
curvature?: number; curvature?: number;
centerX?: number;
centerY?: number;
} }
// @TODO: refactor getBezierPath function. It's too long and hard to understand. interface GetControlWithCurvatureParams {
// We should reuse the curvature handling for top/bottom and left/right. pos: Position;
x1: number;
y1: number;
x2: number;
y2: number;
c: number;
}
function calculateControlOffset(distance: number, curvature: number): number {
if (distance >= 0) {
return 0.5 * distance;
} else {
return curvature * 25 * Math.sqrt(-distance);
}
}
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
let ctX: number, ctY: number;
switch (pos) {
case Position.Left:
{
ctX = x1 - calculateControlOffset(x1 - x2, c);
ctY = y1;
}
break;
case Position.Right:
{
ctX = x1 + calculateControlOffset(x2 - x1, c);
ctY = y1;
}
break;
case Position.Top:
{
ctX = x1;
ctY = y1 - calculateControlOffset(y1 - y2, c);
}
break;
case Position.Bottom:
{
ctX = x1;
ctY = y1 + calculateControlOffset(y2 - y1, c);
}
break;
}
return [ctX, ctY];
}
export function getBezierPath({ export function getBezierPath({
sourceX, sourceX,
sourceY, sourceY,
@@ -25,78 +68,62 @@ export function getBezierPath({
targetY, targetY,
targetPosition = Position.Top, targetPosition = Position.Top,
curvature = 0.25, curvature = 0.25,
centerX,
centerY,
}: GetBezierPathParams): string { }: GetBezierPathParams): string {
const leftAndRight = [Position.Left, Position.Right]; const [sourceControlX, sourceControlY] = getControlWithCurvature({
const hasCurvature = curvature > 0; pos: sourcePosition,
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY }); x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
c: curvature,
});
const [targetControlX, targetControlY] = getControlWithCurvature({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
c: curvature,
});
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`;
}
if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) { // @TODO: this function will recalculate the control points
const cX = typeof centerX !== 'undefined' ? centerX : _centerX; // one option is to let getXXXPath() return center points
const distanceX = targetX - sourceX; // but will introduce breaking changes
const absDistanceX = Math.abs(distanceX); // the getCenter() of other types of edges might need to change, too
const amtX = (Math.sqrt(absDistanceX) / 2) * (50 * curvature); export function getBezierCenter({
sourceX,
let hx1 = cX; sourceY,
let hx2 = cX; sourcePosition = Position.Bottom,
targetX,
if (hasCurvature) { targetY,
const sourceAndTargetRight = sourcePosition === Position.Right && targetPosition === Position.Right; targetPosition = Position.Top,
const sourceAndTargetLeft = sourcePosition === Position.Left && targetPosition === Position.Left; curvature = 0.25,
}: GetBezierPathParams): [number, number, number, number] {
hx1 = sourceX + amtX; const [sourceControlX, sourceControlY] = getControlWithCurvature({
hx2 = targetX - amtX; pos: sourcePosition,
x1: sourceX,
if (sourceAndTargetLeft) { y1: sourceY,
hx1 = sourceX - amtX; x2: targetX,
} else if (sourceAndTargetRight) { y2: targetY,
hx2 = targetX + amtX; c: curvature,
} else if (sourcePosition === Position.Left && targetX <= sourceX) { });
hx1 = cX; const [targetControlX, targetControlY] = getControlWithCurvature({
hx2 = cX; pos: targetPosition,
} else if (sourcePosition === Position.Left && targetX > sourceX) { x1: targetX,
hx1 = sourceX - amtX; y1: targetY,
hx2 = targetX + amtX; x2: sourceX,
} y2: sourceY,
} c: curvature,
});
return `M${sourceX},${sourceY} C${hx1},${sourceY} ${hx2},${targetY}, ${targetX},${targetY}`; // cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
} else if (leftAndRight.includes(targetPosition)) { // https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
return `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`; const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
} else if (leftAndRight.includes(sourcePosition)) { const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
return `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`; const xOffset = Math.abs(centerX - sourceX);
} const yOffset = Math.abs(centerY - sourceY);
return [centerX, centerY, xOffset, yOffset];
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
const distanceY = targetY - sourceY;
const absDistanceY = Math.abs(distanceY);
const amtY = (Math.sqrt(absDistanceY) / 2) * (50 * curvature);
let hy1 = cY;
let hy2 = cY;
if (hasCurvature) {
hy1 = sourceY + amtY;
hy2 = targetY - amtY;
const sourceAndTargetTop = sourcePosition === Position.Top && targetPosition === Position.Top;
const sourceAndTargetBottom = sourcePosition === Position.Bottom && targetPosition === Position.Bottom;
if (sourceAndTargetTop) {
hy1 = targetY - amtY;
} else if (sourceAndTargetBottom) {
hy2 = targetY + amtY;
} else if (sourcePosition === Position.Top && targetY <= sourceY) {
hy1 = cY;
hy2 = cY;
} else if (sourcePosition === Position.Top && targetY > sourceY) {
hy1 = sourceY - amtY;
hy2 = targetY + amtY;
}
}
return `M${sourceX},${sourceY} C${sourceX},${hy1} ${targetX},${hy2} ${targetX},${targetY}`;
} }
export default memo( export default memo(
@@ -118,8 +145,7 @@ export default memo(
markerStart, markerStart,
curvature, curvature,
}: EdgeProps) => { }: EdgeProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition }); const params = {
const path = getBezierPath({
sourceX, sourceX,
sourceY, sourceY,
sourcePosition, sourcePosition,
@@ -127,7 +153,9 @@ export default memo(
targetY, targetY,
targetPosition, targetPosition,
curvature, curvature,
}); };
const path = getBezierPath(params);
const [centerX, centerY] = getBezierCenter(params);
return ( return (
<BaseEdge <BaseEdge
+145 -8
View File
@@ -1,12 +1,149 @@
import React, { memo } from 'react'; import React, { memo } from 'react';
import { EdgeProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
import BezierEdge, { getBezierPath, GetBezierPathParams } from './BezierEdge'; export interface GetSimpleBezierPathParams {
import { EdgeProps } from '../../types'; sourceX: number;
sourceY: number;
export function getSimpleBezierPath(props: GetBezierPathParams): string { sourcePosition?: Position;
return getBezierPath({ ...props, curvature: 0 }); targetX: number;
targetY: number;
targetPosition?: Position;
} }
export default memo((props: EdgeProps) => { interface GetControlParams {
return <BezierEdge {...props} curvature={0} />; pos: Position;
}); x1: number;
y1: number;
x2: number;
y2: number;
}
function getControl({ pos, x1, y1, x2, y2 }: GetControlParams): [number, number] {
let ctX: number, ctY: number;
switch (pos) {
case Position.Left:
case Position.Right:
{
ctX = 0.5 * (x1 + x2);
ctY = y1;
}
break;
case Position.Top:
case Position.Bottom:
{
ctX = x1;
ctY = 0.5 * (y1 + y2);
}
break;
}
return [ctX, ctY];
}
export function getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): string {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
});
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
});
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`;
}
// @TODO: this function will recalculate the control points
// one option is to let getXXXPath() return center points
// but will introduce breaking changes
// the getCenter() of other types of edges might need to change, too
export function getSimpleBezierCenter({
sourceX,
sourceY,
sourcePosition = Position.Bottom,
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): [number, number, number, number] {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
y1: sourceY,
x2: targetX,
y2: targetY,
});
const [targetControlX, targetControlY] = getControl({
pos: targetPosition,
x1: targetX,
y1: targetY,
x2: sourceX,
y2: sourceY,
});
// cubic bezier t=0.5 mid point, not the actual mid point, but easy to calculate
// https://stackoverflow.com/questions/67516101/how-to-find-distance-mid-point-of-bezier-curve
const centerX = sourceX * 0.125 + sourceControlX * 0.375 + targetControlX * 0.375 + targetX * 0.125;
const centerY = sourceY * 0.125 + sourceControlY * 0.375 + targetControlY * 0.375 + targetY * 0.125;
const xOffset = Math.abs(centerX - sourceX);
const yOffset = Math.abs(centerY - sourceY);
return [centerX, centerY, xOffset, yOffset];
}
export default memo(
({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
markerEnd,
markerStart,
}: EdgeProps) => {
const params = {
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
};
const path = getSimpleBezierPath(params);
const [centerX, centerY] = getSimpleBezierCenter(params);
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
style={style}
markerEnd={markerEnd}
markerStart={markerStart}
/>
);
}
);
+3
View File
@@ -155,6 +155,9 @@ export type ConnectionLineComponentProps = {
targetPosition?: Position; targetPosition?: Position;
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
fromNode?: Node;
fromHandle?: HandleElement;
// backward compatibility, mark as deprecated?
sourceNode?: Node; sourceNode?: Node;
sourceHandle?: HandleElement; sourceHandle?: HandleElement;
}; };