refactor(edges): siplify edge path / label position handling, repair edge labels

This commit is contained in:
moklick
2022-09-27 14:17:02 +02:00
parent aef8b477c2
commit e634e7296a
12 changed files with 182 additions and 213 deletions

View File

@@ -77,10 +77,6 @@ const edges: Edge[] = [
id: 'e1-2',
source: '1',
target: '2',
type: 'smoothstep',
markerEnd: {
type: MarkerType.ArrowClosed,
},
pathOptions: {
offset: 30,
},
@@ -90,10 +86,6 @@ const edges: Edge[] = [
id: 'e3-4',
source: '3',
target: '4',
type: 'smoothstep',
markerEnd: {
type: MarkerType.ArrowClosed,
},
pathOptions: {
borderRadius: 2,
},
@@ -104,24 +96,21 @@ const edges: Edge[] = [
id: 'e4-5',
source: '5',
target: '6',
type: 'smoothstep',
markerEnd: {
type: MarkerType.ArrowClosed,
},
},
{
id: 'e7-8',
source: '7',
target: '8',
type: 'smoothstep',
markerEnd: {
type: MarkerType.ArrowClosed,
},
},
];
const defaultEdgeOptions = {
label: 'Edge Label',
type: 'default',
markerEnd: {
type: MarkerType.ArrowClosed,
},
style: {
strokeWidth: 1,
},

View File

@@ -1,4 +1,4 @@
import React, { FC } from 'react';
import { FC } from 'react';
import { EdgeProps, getBezierPath } from 'reactflow';
const CustomEdge: FC<EdgeProps> = ({
@@ -11,7 +11,7 @@ const CustomEdge: FC<EdgeProps> = ({
targetPosition,
data,
}) => {
const edgePath = getBezierPath({
const [edgePath] = getBezierPath({
sourceX,
sourceY,
sourcePosition,

View File

@@ -1,5 +1,5 @@
import React, { FC } from 'react';
import { EdgeProps, getBezierPath, EdgeText, getBezierEdgeCenter } from 'reactflow';
import { FC } from 'react';
import { EdgeProps, getBezierPath, EdgeText } from 'reactflow';
const CustomEdge: FC<EdgeProps> = ({
id,
@@ -11,7 +11,7 @@ const CustomEdge: FC<EdgeProps> = ({
targetPosition,
data,
}) => {
const edgePath = getBezierPath({
const [edgePath, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
@@ -19,19 +19,13 @@ const CustomEdge: FC<EdgeProps> = ({
targetY,
targetPosition,
});
const [centerX, centerY] = getBezierEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
});
return (
<>
<path id={id} className="react-flow__edge-path" d={edgePath} />
<EdgeText
x={centerX}
y={centerY}
x={labelX}
y={labelY}
label={data.text}
labelStyle={{ fill: 'white' }}
labelShowBg

View File

@@ -97,16 +97,16 @@ const ConnectionLine = ({
if (connectionLineType === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
dAttr = getBezierPath(pathParams);
[dAttr] = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) {
dAttr = getSmoothStepPath({
[dAttr] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
dAttr = getSmoothStepPath(pathParams);
[dAttr] = getSmoothStepPath(pathParams);
} else if (connectionLineType === ConnectionLineType.SimpleBezier) {
dAttr = getSimpleBezierPath(pathParams);
[dAttr] = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`;
}

View File

@@ -3,8 +3,8 @@ import { BaseEdgeProps } from '../../types';
const BaseEdge = ({
path,
centerX,
centerY,
labelX,
labelY,
label,
labelStyle,
labelShowBg,
@@ -29,8 +29,8 @@ const BaseEdge = ({
{interactionWidth && <path d={path} fill="none" strokeOpacity={0} strokeWidth={interactionWidth} />}
{label ? (
<EdgeText
x={centerX}
y={centerY}
x={labelX}
y={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}

View File

@@ -1,6 +1,7 @@
import { memo } from 'react';
import { BezierEdgeProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
import { getBezierEdgeCenter } from './utils';
export interface GetBezierPathParams {
sourceX: number;
@@ -68,7 +69,7 @@ export function getBezierPath({
targetY,
targetPosition = Position.Top,
curvature = 0.25,
}: GetBezierPathParams): string {
}: GetBezierPathParams): [string, number, number, number, number] {
const [sourceControlX, sourceControlY] = getControlWithCurvature({
pos: sourcePosition,
x1: sourceX,
@@ -85,45 +86,24 @@ export function getBezierPath({
y2: sourceY,
c: curvature,
});
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`;
}
const [centerX, centerY, offsetX, offsetY] = getBezierEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
sourceControlX,
sourceControlY,
targetControlX,
targetControlY,
});
// @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];
return [
`M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
centerX,
centerY,
offsetX,
offsetY,
];
}
const BezierEdge = memo(
@@ -146,7 +126,7 @@ const BezierEdge = memo(
pathOptions,
interactionWidth,
}: BezierEdgeProps) => {
const params = {
const [path, labelX, labelY] = getBezierPath({
sourceX,
sourceY,
sourcePosition,
@@ -154,15 +134,13 @@ const BezierEdge = memo(
targetY,
targetPosition,
curvature: pathOptions?.curvature,
};
const path = getBezierPath(params);
const [centerX, centerY] = getBezierCenter(params);
});
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
labelX={labelX}
labelY={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}

View File

@@ -1,6 +1,7 @@
import { memo } from 'react';
import { EdgeProps, Position } from '../../types';
import BaseEdge from './BaseEdge';
import { getBezierEdgeCenter } from './utils';
export interface GetSimpleBezierPathParams {
sourceX: number;
@@ -47,7 +48,7 @@ export function getSimpleBezierPath({
targetX,
targetY,
targetPosition = Position.Top,
}: GetSimpleBezierPathParams): string {
}: GetSimpleBezierPathParams): [string, number, number, number, number] {
const [sourceControlX, sourceControlY] = getControl({
pos: sourcePosition,
x1: sourceX,
@@ -62,42 +63,24 @@ export function getSimpleBezierPath({
x2: sourceX,
y2: sourceY,
});
return `M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`;
}
const [centerX, centerY, offsetX, offsetY] = getBezierEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
sourceControlX,
sourceControlY,
targetControlX,
targetControlY,
});
// @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];
return [
`M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
centerX,
centerY,
offsetX,
offsetY,
];
}
const SimpleBezierEdge = memo(
@@ -119,22 +102,20 @@ const SimpleBezierEdge = memo(
markerStart,
interactionWidth,
}: EdgeProps) => {
const params = {
const [path, labelX, labelY] = getSimpleBezierPath({
sourceX,
sourceY,
sourcePosition,
targetX,
targetY,
targetPosition,
};
const path = getSimpleBezierPath(params);
const [centerX, centerY] = getSimpleBezierCenter(params);
});
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
labelX={labelX}
labelY={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}

View File

@@ -1,8 +1,8 @@
import { memo } from 'react';
import { getCenter } from './utils';
import { SmoothStepEdgeProps, Position, XYPosition } from '../../types';
import BaseEdge from './BaseEdge';
import { getSimpleEdgeCenter } from './utils';
export interface GetSmoothStepPathParams {
sourceX: number;
@@ -55,9 +55,9 @@ function getPoints({
sourcePosition: Position;
target: XYPosition;
targetPosition: Position;
center: XYPosition;
center: Partial<XYPosition>;
offset: number;
}): XYPosition[] {
}): [XYPosition[], number, number, number, number] {
const sourceDir = handleDirections[sourcePosition];
const targetDir = handleDirections[targetPosition];
const sourceGapped: XYPosition = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset };
@@ -71,22 +71,31 @@ function getPoints({
const currDir = dir[dirAccessor];
let points: XYPosition[] = [];
let centerX, centerY;
const [defaultCenterX, defaultCenterY, defaultOffsetX, defaultOffsetY] = getSimpleEdgeCenter({
sourceX: source.x,
sourceY: source.y,
targetX: target.x,
targetY: target.y,
});
// opposite handle positions, default case
if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) {
centerX = center.x || defaultCenterX;
centerY = center.y || defaultCenterY;
// --->
// |
// >---
const verticalSplit: XYPosition[] = [
{ x: center.x, y: sourceGapped.y },
{ x: center.x, y: targetGapped.y },
{ x: centerX, y: sourceGapped.y },
{ x: centerX, y: targetGapped.y },
];
// |
// ---
// |
const horizontalSplit: XYPosition[] = [
{ x: sourceGapped.x, y: center.y },
{ x: targetGapped.x, y: center.y },
{ x: sourceGapped.x, y: centerY },
{ x: targetGapped.x, y: centerY },
];
if (sourceDir[dirAccessor] === currDir) {
@@ -119,9 +128,14 @@ function getPoints({
points = dirAccessor === 'x' ? sourceTarget : targetSource;
}
}
centerX = points[0].x;
centerY = points[0].y;
}
return [source, sourceGapped, ...points, targetGapped, target];
const pathPoints = [source, sourceGapped, ...points, targetGapped, target];
return [pathPoints, centerX, centerY, defaultOffsetX, defaultOffsetY];
}
function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): string {
@@ -156,21 +170,17 @@ export function getSmoothStepPath({
centerX,
centerY,
offset = 20,
}: GetSmoothStepPathParams): string {
const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY });
const cX = typeof centerX !== 'undefined' ? centerX : _centerX;
const cY = typeof centerY !== 'undefined' ? centerY : _centerY;
const points = getPoints({
}: GetSmoothStepPathParams): [string, number, number, number, number] {
const [points, labelX, labelY, offsetX, offsetY] = getPoints({
source: { x: sourceX, y: sourceY },
sourcePosition,
target: { x: targetX, y: targetY },
targetPosition,
center: { x: cX, y: cY },
center: { x: centerX, y: centerY },
offset,
});
return points.reduce<string>((res, p, i) => {
const path = points.reduce<string>((res, p, i) => {
let segment = '';
if (i > 0 && i < points.length - 1) {
@@ -183,6 +193,8 @@ export function getSmoothStepPath({
return res;
}, '');
return [path, labelX, labelY, offsetX, offsetY];
}
const SmoothStepEdge = memo(
@@ -205,9 +217,7 @@ const SmoothStepEdge = memo(
pathOptions,
interactionWidth,
}: SmoothStepEdgeProps) => {
const [centerX, centerY] = getCenter({ sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition });
const path = getSmoothStepPath({
const [path, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition,
@@ -221,8 +231,8 @@ const SmoothStepEdge = memo(
return (
<BaseEdge
path={path}
centerX={centerX}
centerY={centerY}
labelX={labelX}
labelY={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}

View File

@@ -2,6 +2,30 @@ import { memo } from 'react';
import BaseEdge from './BaseEdge';
import { EdgeProps } from '../../types';
import { getSimpleEdgeCenter } from './utils';
export type GetStraightPathParams = {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
};
export function getStraightPath({
sourceX,
sourceY,
targetX,
targetY,
}: GetStraightPathParams): [string, number, number, number, number] {
const [centerX, centerY, offsetX, offsetY] = getSimpleEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
});
return [`M ${sourceX},${sourceY}L ${targetX},${targetY}`, centerX, centerY, offsetX, offsetY];
}
const StraightEdge = memo(
({
@@ -20,17 +44,13 @@ const StraightEdge = memo(
markerStart,
interactionWidth,
}: EdgeProps) => {
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
const [path, labelX, labelY] = getStraightPath({ sourceX, sourceY, targetX, targetY });
return (
<BaseEdge
path={`M ${sourceX},${sourceY}L ${targetX},${targetY}`}
centerX={centerX}
centerY={centerY}
path={path}
labelX={labelX}
labelY={labelY}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}

View File

@@ -1,7 +1,7 @@
import { MouseEvent as ReactMouseEvent } from 'react';
import { StoreApi } from 'zustand';
import { Edge, MarkerType, Position, ReactFlowState } from '../../types';
import { Edge, MarkerType, ReactFlowState } from '../../types';
export const getMarkerEnd = (markerType?: MarkerType, markerEndId?: string): string => {
if (typeof markerEndId !== 'undefined' && markerEndId) {
@@ -11,51 +11,6 @@ export const getMarkerEnd = (markerType?: MarkerType, markerEndId?: string): str
return typeof markerType !== 'undefined' ? `url(#react-flow__${markerType})` : 'none';
};
export interface GetCenterParams {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourcePosition?: Position;
targetPosition?: Position;
}
const LeftOrRight = [Position.Left, Position.Right];
export const getCenter = ({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition = Position.Bottom,
targetPosition = Position.Top,
}: GetCenterParams): [number, number, number, number] => {
const sourceIsLeftOrRight = LeftOrRight.includes(sourcePosition);
const targetIsLeftOrRight = LeftOrRight.includes(targetPosition);
// we expect flows to be horizontal or vertical (all handles left or right respectively top or bottom)
// a mixed edge is when one the source is on the left and the target is on the top for example.
const mixedEdge = (sourceIsLeftOrRight && !targetIsLeftOrRight) || (targetIsLeftOrRight && !sourceIsLeftOrRight);
if (mixedEdge) {
const xOffset = sourceIsLeftOrRight ? Math.abs(targetX - sourceX) : 0;
const centerX = sourceX > targetX ? sourceX - xOffset : sourceX + xOffset;
const yOffset = sourceIsLeftOrRight ? 0 : Math.abs(targetY - sourceY);
const centerY = sourceY < targetY ? sourceY + yOffset : sourceY - yOffset;
return [centerX, centerY, xOffset, yOffset];
}
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
return [centerX, centerY, xOffset, yOffset];
};
export function getMouseHandler(
id: string,
getState: StoreApi<ReactFlowState>['getState'],
@@ -71,3 +26,53 @@ export function getMouseHandler(
}
};
}
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
export function getSimpleEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
}: {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
}): [number, number, number, number] {
const xOffset = Math.abs(targetX - sourceX) / 2;
const centerX = targetX < sourceX ? targetX + xOffset : targetX - xOffset;
const yOffset = Math.abs(targetY - sourceY) / 2;
const centerY = targetY < sourceY ? targetY + yOffset : targetY - yOffset;
return [centerX, centerY, xOffset, yOffset];
}
export function getBezierEdgeCenter({
sourceX,
sourceY,
targetX,
targetY,
sourceControlX,
sourceControlY,
targetControlX,
targetControlY,
}: {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
sourceControlX: number;
sourceControlY: number;
targetControlX: number;
targetControlY: number;
}): [number, number, number, number] {
// 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 offsetX = Math.abs(centerX - sourceX);
const offsetY = Math.abs(centerY - sourceY);
return [centerX, centerY, offsetX, offsetY];
}

View File

@@ -1,18 +1,10 @@
export { default as ReactFlow } from './container/ReactFlow';
export { default as Handle } from './components/Handle';
export { default as EdgeText } from './components/Edges/EdgeText';
export { default as StraightEdge } from './components/Edges/StraightEdge';
export { default as StraightEdge, getStraightPath } from './components/Edges/StraightEdge';
export { default as StepEdge } from './components/Edges/StepEdge';
export {
default as BezierEdge,
getBezierPath,
getBezierCenter as getBezierEdgeCenter,
} from './components/Edges/BezierEdge';
export {
default as SimpleBezierEdge,
getSimpleBezierPath,
getSimpleBezierCenter as getSimpleBezierEdgeCenter,
} from './components/Edges/SimpleBezierEdge';
export { default as BezierEdge, getBezierPath } from './components/Edges/BezierEdge';
export { default as SimpleBezierEdge, getSimpleBezierPath } from './components/Edges/SimpleBezierEdge';
export { default as SmoothStepEdge, getSmoothStepPath } from './components/Edges/SmoothStepEdge';
export { default as BaseEdge } from './components/Edges/BaseEdge';
@@ -29,7 +21,7 @@ export {
getRectOfNodes,
} from './utils/graph';
export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
export { getMarkerEnd, getCenter as getEdgeCenter } from './components/Edges/utils';
export { getMarkerEnd } from './components/Edges/utils';
export { default as ReactFlowProvider } from './components/ReactFlowProvider';
export { default as Panel } from './components/Panel';

View File

@@ -104,8 +104,8 @@ export type BaseEdgeProps = Pick<
| 'markerEnd'
| 'interactionWidth'
> & {
centerX: number;
centerY: number;
labelX: number;
labelY: number;
path: string;
};