Merge pull request #1984 from Sec-ant/refactor-bezier-function
refactor Bezier function
This commit is contained in:
@@ -4,15 +4,7 @@ import shallow from 'zustand/shallow';
|
||||
import { useStore } from '../../store';
|
||||
import { getBezierPath } from '../Edges/BezierEdge';
|
||||
import { getSmoothStepPath } from '../Edges/SmoothStepEdge';
|
||||
import {
|
||||
HandleElement,
|
||||
ConnectionLineType,
|
||||
ConnectionLineComponent,
|
||||
HandleType,
|
||||
Node,
|
||||
ReactFlowState,
|
||||
Position,
|
||||
} from '../../types';
|
||||
import { ConnectionLineType, ConnectionLineComponent, HandleType, Node, ReactFlowState, Position } from '../../types';
|
||||
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
|
||||
|
||||
interface ConnectionLineProps {
|
||||
@@ -29,13 +21,6 @@ interface ConnectionLineProps {
|
||||
|
||||
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 ({
|
||||
connectionNodeId,
|
||||
connectionHandleId,
|
||||
@@ -51,28 +36,74 @@ export default ({
|
||||
const handleId = connectionHandleId;
|
||||
|
||||
const { nodeInternals, transform } = useStore(selector, shallow);
|
||||
const sourceNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
|
||||
const fromNode = useRef<Node | undefined>(nodeInternals.get(nodeId));
|
||||
|
||||
if (
|
||||
!sourceNode.current ||
|
||||
!sourceNode.current ||
|
||||
!fromNode.current ||
|
||||
!fromNode.current ||
|
||||
!isConnectable ||
|
||||
!sourceNode.current.handleBounds?.[connectionHandleType]
|
||||
!fromNode.current.handleBounds?.[connectionHandleType]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceHandle = getSourceHandle(handleId, sourceNode.current, connectionHandleType);
|
||||
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : (sourceNode.current?.width ?? 0) / 2;
|
||||
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.current?.height ?? 0;
|
||||
const sourceX = (sourceNode.current.positionAbsolute?.x || 0) + sourceHandleX;
|
||||
const sourceY = (sourceNode.current.positionAbsolute?.y || 0) + sourceHandleY;
|
||||
const handleBound = fromNode.current.handleBounds?.[connectionHandleType];
|
||||
const fromHandle = handleId ? handleBound?.find((d) => d.id === handleId) : handleBound?.[0];
|
||||
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.current?.width ?? 0) / 2;
|
||||
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.current?.height ?? 0;
|
||||
const fromX = (fromNode.current.positionAbsolute?.x || 0) + fromHandleX;
|
||||
const fromY = (fromNode.current.positionAbsolute?.y || 0) + fromHandleY;
|
||||
|
||||
const targetX = (connectionPositionX - transform[0]) / transform[2];
|
||||
const targetY = (connectionPositionY - transform[1]) / transform[2];
|
||||
const toX = (connectionPositionX - transform[0]) / transform[2];
|
||||
const toY = (connectionPositionY - transform[1]) / transform[2];
|
||||
|
||||
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right;
|
||||
const targetPosition = isRightOrLeft ? Position.Left : Position.Top;
|
||||
const fromPosition = fromHandle?.position;
|
||||
|
||||
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) {
|
||||
return (
|
||||
@@ -80,14 +111,17 @@ export default ({
|
||||
<CustomConnectionLineComponent
|
||||
sourceX={sourceX}
|
||||
sourceY={sourceY}
|
||||
sourcePosition={sourceHandle?.position}
|
||||
sourcePosition={sourcePosition}
|
||||
targetX={targetX}
|
||||
targetY={targetY}
|
||||
targetPosition={targetPosition}
|
||||
connectionLineType={connectionLineType}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
sourceNode={sourceNode.current as Node}
|
||||
sourceHandle={sourceHandle}
|
||||
fromNode={fromNode.current}
|
||||
fromHandle={fromHandle}
|
||||
// backward compatibility, mark as deprecated?
|
||||
sourceNode={fromNode.current}
|
||||
sourceHandle={fromHandle}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
@@ -98,16 +132,15 @@ export default ({
|
||||
const pathParams = {
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition: sourceHandle?.position,
|
||||
sourcePosition,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition,
|
||||
};
|
||||
|
||||
if (connectionLineType === ConnectionLineType.Bezier) {
|
||||
// @TODO: we need another getBezier function, that handles a connection line.
|
||||
// Since we don't know the target position, we can't use the default bezier function here.
|
||||
dAttr = getBezierPath({ ...pathParams, curvature: 0 });
|
||||
// we assume the destination position is opposite to the source position
|
||||
dAttr = getBezierPath(pathParams);
|
||||
} else if (connectionLineType === ConnectionLineType.Step) {
|
||||
dAttr = getSmoothStepPath({
|
||||
...pathParams,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { memo } from 'react';
|
||||
import { EdgeProps, Position } from '../../types';
|
||||
import BaseEdge from './BaseEdge';
|
||||
import { getCenter } from './utils';
|
||||
|
||||
export interface GetBezierPathParams {
|
||||
sourceX: number;
|
||||
@@ -11,12 +10,56 @@ export interface GetBezierPathParams {
|
||||
targetY: number;
|
||||
targetPosition?: Position;
|
||||
curvature?: number;
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
}
|
||||
|
||||
// @TODO: refactor getBezierPath function. It's too long and hard to understand.
|
||||
// We should reuse the curvature handling for top/bottom and left/right.
|
||||
interface GetControlWithCurvatureParams {
|
||||
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({
|
||||
sourceX,
|
||||
sourceY,
|
||||
@@ -25,78 +68,62 @@ export function getBezierPath({
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
curvature = 0.25,
|
||||
centerX,
|
||||
centerY,
|
||||
}: GetBezierPathParams): string {
|
||||
const leftAndRight = [Position.Left, Position.Right];
|
||||
const hasCurvature = curvature > 0;
|
||||
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
|
||||
const [sourceControlX, sourceControlY] = getControlWithCurvature({
|
||||
pos: sourcePosition,
|
||||
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)) {
|
||||
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
|
||||
const distanceX = targetX - sourceX;
|
||||
const absDistanceX = Math.abs(distanceX);
|
||||
const amtX = (Math.sqrt(absDistanceX) / 2) * (50 * curvature);
|
||||
|
||||
let hx1 = cX;
|
||||
let hx2 = cX;
|
||||
|
||||
if (hasCurvature) {
|
||||
const sourceAndTargetRight = sourcePosition === Position.Right && targetPosition === Position.Right;
|
||||
const sourceAndTargetLeft = sourcePosition === Position.Left && targetPosition === Position.Left;
|
||||
|
||||
hx1 = sourceX + amtX;
|
||||
hx2 = targetX - amtX;
|
||||
|
||||
if (sourceAndTargetLeft) {
|
||||
hx1 = sourceX - amtX;
|
||||
} else if (sourceAndTargetRight) {
|
||||
hx2 = targetX + amtX;
|
||||
} else if (sourcePosition === Position.Left && targetX <= sourceX) {
|
||||
hx1 = cX;
|
||||
hx2 = cX;
|
||||
} else if (sourcePosition === Position.Left && targetX > sourceX) {
|
||||
hx1 = sourceX - amtX;
|
||||
hx2 = targetX + amtX;
|
||||
}
|
||||
}
|
||||
|
||||
return `M${sourceX},${sourceY} C${hx1},${sourceY} ${hx2},${targetY}, ${targetX},${targetY}`;
|
||||
} else if (leftAndRight.includes(targetPosition)) {
|
||||
return `M${sourceX},${sourceY} Q${sourceX},${targetY} ${targetX},${targetY}`;
|
||||
} else if (leftAndRight.includes(sourcePosition)) {
|
||||
return `M${sourceX},${sourceY} Q${targetX},${sourceY} ${targetX},${targetY}`;
|
||||
}
|
||||
|
||||
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}`;
|
||||
// @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 getBezierCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
curvature = 0.25,
|
||||
}: GetBezierPathParams): [number, number, number, number] {
|
||||
const [sourceControlX, sourceControlY] = getControlWithCurvature({
|
||||
pos: sourcePosition,
|
||||
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,
|
||||
});
|
||||
// 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(
|
||||
@@ -118,8 +145,7 @@ export default memo(
|
||||
markerStart,
|
||||
curvature,
|
||||
}: EdgeProps) => {
|
||||
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
|
||||
const path = getBezierPath({
|
||||
const params = {
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition,
|
||||
@@ -127,7 +153,9 @@ export default memo(
|
||||
targetY,
|
||||
targetPosition,
|
||||
curvature,
|
||||
});
|
||||
};
|
||||
const path = getBezierPath(params);
|
||||
const [centerX, centerY] = getBezierCenter(params);
|
||||
|
||||
return (
|
||||
<BaseEdge
|
||||
|
||||
@@ -1,12 +1,149 @@
|
||||
import React, { memo } from 'react';
|
||||
import { EdgeProps, Position } from '../../types';
|
||||
import BaseEdge from './BaseEdge';
|
||||
|
||||
import BezierEdge, { getBezierPath, GetBezierPathParams } from './BezierEdge';
|
||||
import { EdgeProps } from '../../types';
|
||||
|
||||
export function getSimpleBezierPath(props: GetBezierPathParams): string {
|
||||
return getBezierPath({ ...props, curvature: 0 });
|
||||
export interface GetSimpleBezierPathParams {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourcePosition?: Position;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
targetPosition?: Position;
|
||||
}
|
||||
|
||||
export default memo((props: EdgeProps) => {
|
||||
return <BezierEdge {...props} curvature={0} />;
|
||||
});
|
||||
interface GetControlParams {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -155,6 +155,9 @@ export type ConnectionLineComponentProps = {
|
||||
targetPosition?: Position;
|
||||
connectionLineStyle?: CSSProperties;
|
||||
connectionLineType: ConnectionLineType;
|
||||
fromNode?: Node;
|
||||
fromHandle?: HandleElement;
|
||||
// backward compatibility, mark as deprecated?
|
||||
sourceNode?: Node;
|
||||
sourceHandle?: HandleElement;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user