diff --git a/packages/vue-flow/src/components/Edges/SmoothStepEdge.ts b/packages/vue-flow/src/components/Edges/SmoothStepEdge.ts index 4dcc8b82..b15e4d0a 100644 --- a/packages/vue-flow/src/components/Edges/SmoothStepEdge.ts +++ b/packages/vue-flow/src/components/Edges/SmoothStepEdge.ts @@ -20,6 +20,7 @@ const SmoothStepEdge: FunctionalComponent = function ({ markerEnd, markerStart, borderRadius, + offset, style, }) { const [centerX, centerY] = getCenter({ @@ -39,6 +40,7 @@ const SmoothStepEdge: FunctionalComponent = function ({ sourcePosition, targetPosition, borderRadius, + offset, }) return h(BaseEdge, { @@ -71,6 +73,7 @@ SmoothStepEdge.props = [ 'targetX', 'targetY', 'borderRadius', + 'offset', 'markerEnd', 'markerStart', 'style', diff --git a/packages/vue-flow/src/components/Edges/utils/smoothstep.ts b/packages/vue-flow/src/components/Edges/utils/smoothstep.ts index 54225d31..f2347af6 100644 --- a/packages/vue-flow/src/components/Edges/utils/smoothstep.ts +++ b/packages/vue-flow/src/components/Edges/utils/smoothstep.ts @@ -1,4 +1,5 @@ import { getCenter } from './general' +import type { XYPosition } from '~/types' import { Position } from '~/types' export interface GetSmoothStepPathParams { @@ -11,20 +12,136 @@ export interface GetSmoothStepPathParams { borderRadius?: number centerX?: number centerY?: number + offset?: number } -// These are some helper methods for drawing the round corners -// The name indicates the direction of the path. "bottomLeftCorner" goes -// from bottom to the left and "leftBottomCorner" goes from left to the bottom. -// We have to consider the direction of the paths because of the animated lines. -const bottomLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x + size},${y}` -const leftBottomCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y - size}` -const bottomRightCorner = (x: number, y: number, size: number): string => `L ${x},${y - size}Q ${x},${y} ${x - size},${y}` -const rightBottomCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y - size}` -const leftTopCorner = (x: number, y: number, size: number): string => `L ${x + size},${y}Q ${x},${y} ${x},${y + size}` -const topLeftCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x + size},${y}` -const topRightCorner = (x: number, y: number, size: number): string => `L ${x},${y + size}Q ${x},${y} ${x - size},${y}` -const rightTopCorner = (x: number, y: number, size: number): string => `L ${x - size},${y}Q ${x},${y} ${x},${y + size}` +const handleDirections = { + [Position.Left]: { x: -1, y: 0 }, + [Position.Right]: { x: 1, y: 0 }, + [Position.Top]: { x: 0, y: -1 }, + [Position.Bottom]: { x: 0, y: 1 }, +} + +const getDirection = ({ + source, + sourcePosition = Position.Bottom, + target, +}: { + source: XYPosition + sourcePosition: Position + target: XYPosition +}): XYPosition => { + if (sourcePosition === Position.Left || sourcePosition === Position.Right) { + return source.x < target.x ? { x: 1, y: 0 } : { x: -1, y: 0 } + } + return source.y < target.y ? { x: 0, y: 1 } : { x: 0, y: -1 } +} + +const distance = (a: XYPosition, b: XYPosition) => Math.sqrt((b.x - a.x) ** 2 + (b.y - a.y) ** 2) + +// With this function we try to mimic an orthogonal edge routing behaviour +// It's not as good as a real orthogonal edge routing, but it's faster and good enough as a default for step and smooth step edges +function getPoints({ + source, + sourcePosition = Position.Bottom, + target, + targetPosition = Position.Top, + center, + offset, +}: { + source: XYPosition + sourcePosition: Position + target: XYPosition + targetPosition: Position + center: XYPosition + offset: number +}): XYPosition[] { + const sourceDir = handleDirections[sourcePosition] + const targetDir = handleDirections[targetPosition] + const sourceGapped: XYPosition = { x: source.x + sourceDir.x * offset, y: source.y + sourceDir.y * offset } + const targetGapped: XYPosition = { x: target.x + targetDir.x * offset, y: target.y + targetDir.y * offset } + const dir = getDirection({ + source: sourceGapped, + sourcePosition, + target: targetGapped, + }) + const dirAccessor = dir.x !== 0 ? 'x' : 'y' + const currDir = dir[dirAccessor] + + let points: XYPosition[] = [] + + // opposite handle positions, default case + if (sourceDir[dirAccessor] * targetDir[dirAccessor] === -1) { + // ---> + // | + // >--- + const verticalSplit: XYPosition[] = [ + { x: center.x, y: sourceGapped.y }, + { x: center.x, y: targetGapped.y }, + ] + // | + // --- + // | + const horizontalSplit: XYPosition[] = [ + { x: sourceGapped.x, y: center.y }, + { x: targetGapped.x, y: center.y }, + ] + + if (sourceDir[dirAccessor] === currDir) { + points = dirAccessor === 'x' ? verticalSplit : horizontalSplit + } else { + points = dirAccessor === 'x' ? horizontalSplit : verticalSplit + } + } else { + // sourceTarget means we take x from source and y from target, targetSource is the opposite + const sourceTarget: XYPosition[] = [{ x: sourceGapped.x, y: targetGapped.y }] + const targetSource: XYPosition[] = [{ x: targetGapped.x, y: sourceGapped.y }] + // this handles edges with same handle positions + if (dirAccessor === 'x') { + points = sourceDir.x === currDir ? targetSource : sourceTarget + } else { + points = sourceDir.y === currDir ? sourceTarget : targetSource + } + + // these are conditions for handling mixed handle positions like Right -> Bottom for example + if (sourcePosition !== targetPosition) { + const dirAccessorOpposite = dirAccessor === 'x' ? 'y' : 'x' + const isSameDir = sourceDir[dirAccessor] === targetDir[dirAccessorOpposite] + const sourceGtTargetOppo = sourceGapped[dirAccessorOpposite] > targetGapped[dirAccessorOpposite] + const sourceLtTargetOppo = sourceGapped[dirAccessorOpposite] < targetGapped[dirAccessorOpposite] + const flipSourceTarget = + (sourceDir[dirAccessor] === 1 && ((!isSameDir && sourceGtTargetOppo) || (isSameDir && sourceLtTargetOppo))) || + (sourceDir[dirAccessor] !== 1 && ((!isSameDir && sourceLtTargetOppo) || (isSameDir && sourceGtTargetOppo))) + + if (flipSourceTarget) { + points = dirAccessor === 'x' ? sourceTarget : targetSource + } + } + } + + return [source, sourceGapped, ...points, targetGapped, target] +} + +function getBend(a: XYPosition, b: XYPosition, c: XYPosition, size: number): string { + const bendSize = Math.min(distance(a, b) / 2, distance(b, c) / 2, size) + const { x, y } = b + + // no bend + if ((a.x === x && x === c.x) || (a.y === y && y === c.y)) { + return `L${x} ${y}` + } + + // first segment is horizontal + if (a.y === y) { + const xDir = a.x < c.x ? -1 : 1 + const yDir = a.y < c.y ? 1 : -1 + return `L ${x + bendSize * xDir},${y}Q ${x},${y} ${x},${y + bendSize * yDir}` + } + + const xDir = a.x < c.x ? 1 : -1 + const yDir = a.y < c.y ? -1 : 1 + return `L ${x},${y + bendSize * yDir}Q ${x},${y} ${x + bendSize * xDir},${y}` +} export function getSmoothStepPath({ sourceX, @@ -36,58 +153,32 @@ export function getSmoothStepPath({ borderRadius = 5, centerX, centerY, -}: GetSmoothStepPathParams): string { - const [_centerX, _centerY, offsetX, offsetY] = getCenter({ sourceX, sourceY, targetX, targetY }) - const cornerWidth = Math.min(borderRadius, Math.abs(targetX - sourceX)) - const cornerHeight = Math.min(borderRadius, Math.abs(targetY - sourceY)) - const cornerSize = Math.min(cornerWidth, cornerHeight, offsetX, offsetY) - const leftAndRight = [Position.Left, Position.Right] + offset = 20, +}: GetSmoothStepPathParams) { + const [_centerX, _centerY] = getCenter({ sourceX, sourceY, targetX, targetY }) const cX = typeof centerX !== 'undefined' ? centerX : _centerX const cY = typeof centerY !== 'undefined' ? centerY : _centerY - let firstCornerPath - let secondCornerPath + const points = getPoints({ + source: { x: sourceX, y: sourceY }, + sourcePosition, + target: { x: targetX, y: targetY }, + targetPosition, + center: { x: cX, y: cY }, + offset, + }) - if (sourceX <= targetX) { - firstCornerPath = sourceY <= targetY ? bottomLeftCorner(sourceX, cY, cornerSize) : topLeftCorner(sourceX, cY, cornerSize) - secondCornerPath = sourceY <= targetY ? rightTopCorner(targetX, cY, cornerSize) : rightBottomCorner(targetX, cY, cornerSize) - } else { - firstCornerPath = sourceY < targetY ? bottomRightCorner(sourceX, cY, cornerSize) : topRightCorner(sourceX, cY, cornerSize) - secondCornerPath = sourceY < targetY ? leftTopCorner(targetX, cY, cornerSize) : leftBottomCorner(targetX, cY, cornerSize) - } + return points.reduce((res, p, i) => { + let segment = '' - if (leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) { - if (sourceX <= targetX) { - firstCornerPath = sourceY <= targetY ? rightTopCorner(cX, sourceY, cornerSize) : rightBottomCorner(cX, sourceY, cornerSize) - secondCornerPath = sourceY <= targetY ? bottomLeftCorner(cX, targetY, cornerSize) : topLeftCorner(cX, targetY, cornerSize) - } else if ( - (sourcePosition === Position.Right && targetPosition === Position.Left) || - (sourcePosition === Position.Left && targetPosition === Position.Right) || - (sourcePosition === Position.Left && targetPosition === Position.Left) - ) { - // and sourceX > targetX - firstCornerPath = sourceY <= targetY ? leftTopCorner(cX, sourceY, cornerSize) : leftBottomCorner(cX, sourceY, cornerSize) - secondCornerPath = sourceY <= targetY ? bottomRightCorner(cX, targetY, cornerSize) : topRightCorner(cX, targetY, cornerSize) - } - } else if (leftAndRight.includes(sourcePosition) && !leftAndRight.includes(targetPosition)) { - if (sourceX <= targetX) { - firstCornerPath = - sourceY <= targetY ? rightTopCorner(targetX, sourceY, cornerSize) : rightBottomCorner(targetX, sourceY, cornerSize) + if (i > 0 && i < points.length - 1) { + segment = getBend(points[i - 1], p, points[i + 1], borderRadius) } else { - firstCornerPath = - sourceY <= targetY ? leftTopCorner(targetX, sourceY, cornerSize) : leftBottomCorner(targetX, sourceY, cornerSize) + segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}` } - secondCornerPath = '' - } else if (!leftAndRight.includes(sourcePosition) && leftAndRight.includes(targetPosition)) { - if (sourceX <= targetX) { - firstCornerPath = - sourceY <= targetY ? bottomLeftCorner(sourceX, targetY, cornerSize) : topLeftCorner(sourceX, targetY, cornerSize) - } else { - firstCornerPath = - sourceY <= targetY ? bottomRightCorner(sourceX, targetY, cornerSize) : topRightCorner(sourceX, targetY, cornerSize) - } - secondCornerPath = '' - } - return `M ${sourceX},${sourceY}${firstCornerPath}${secondCornerPath}L ${targetX},${targetY}` + res += segment + + return res + }, '') } diff --git a/packages/vue-flow/src/types/edge.ts b/packages/vue-flow/src/types/edge.ts index 83e275d4..75f0d942 100644 --- a/packages/vue-flow/src/types/edge.ts +++ b/packages/vue-flow/src/types/edge.ts @@ -177,6 +177,7 @@ export interface SmoothStepEdgeProps exte markerStart?: string markerEnd?: string borderRadius?: number + offset?: number data?: Data /** contextual and custom events of edge */ events?: Partial>