refactor(packages): change package structure
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
import { Position } from '../../types';
|
||||
|
||||
export type GetBezierPathParams = {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourcePosition?: Position;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
targetPosition?: Position;
|
||||
curvature?: number;
|
||||
};
|
||||
|
||||
export type GetControlWithCurvatureParams = {
|
||||
pos: Position;
|
||||
x1: number;
|
||||
y1: number;
|
||||
x2: number;
|
||||
y2: number;
|
||||
c: number;
|
||||
};
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
function calculateControlOffset(distance: number, curvature: number): number {
|
||||
if (distance >= 0) {
|
||||
return 0.5 * distance;
|
||||
}
|
||||
|
||||
return curvature * 25 * Math.sqrt(-distance);
|
||||
}
|
||||
|
||||
function getControlWithCurvature({ pos, x1, y1, x2, y2, c }: GetControlWithCurvatureParams): [number, number] {
|
||||
switch (pos) {
|
||||
case Position.Left:
|
||||
return [x1 - calculateControlOffset(x1 - x2, c), y1];
|
||||
case Position.Right:
|
||||
return [x1 + calculateControlOffset(x2 - x1, c), y1];
|
||||
case Position.Top:
|
||||
return [x1, y1 - calculateControlOffset(y1 - y2, c)];
|
||||
case Position.Bottom:
|
||||
return [x1, y1 + calculateControlOffset(y2 - y1, c)];
|
||||
}
|
||||
}
|
||||
|
||||
export function getBezierPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
curvature = 0.25,
|
||||
}: GetBezierPathParams): [path: string, labelX: number, labelY: number, offsetX: number, offsetY: 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,
|
||||
});
|
||||
const [labelX, labelY, offsetX, offsetY] = getBezierEdgeCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
sourceControlX,
|
||||
sourceControlY,
|
||||
targetControlX,
|
||||
targetControlY,
|
||||
});
|
||||
|
||||
return [
|
||||
`M${sourceX},${sourceY} C${sourceControlX},${sourceControlY} ${targetControlX},${targetControlY} ${targetX},${targetY}`,
|
||||
labelX,
|
||||
labelY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Position, type HandleElement, type MarkerType, type Rect, type XYPosition } from '../../types';
|
||||
|
||||
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
|
||||
export function getEdgeCenter({
|
||||
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 const getMarkerEnd = (markerType?: MarkerType, markerEndId?: string): string => {
|
||||
if (typeof markerEndId !== 'undefined' && markerEndId) {
|
||||
return `url(#${markerEndId})`;
|
||||
}
|
||||
|
||||
return typeof markerType !== 'undefined' ? `url(#react-flow__${markerType})` : 'none';
|
||||
};
|
||||
|
||||
export function getHandlePosition(position: Position, nodeRect: Rect, handle: HandleElement | null = null): XYPosition {
|
||||
const x = (handle?.x || 0) + nodeRect.x;
|
||||
const y = (handle?.y || 0) + nodeRect.y;
|
||||
const width = handle?.width || nodeRect.width;
|
||||
const height = handle?.height || nodeRect.height;
|
||||
|
||||
switch (position) {
|
||||
case Position.Top:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y,
|
||||
};
|
||||
case Position.Right:
|
||||
return {
|
||||
x: x + width,
|
||||
y: y + height / 2,
|
||||
};
|
||||
case Position.Bottom:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y: y + height,
|
||||
};
|
||||
case Position.Left:
|
||||
return {
|
||||
x,
|
||||
y: y + height / 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null {
|
||||
if (!bounds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (bounds.length === 1 || !handleId) {
|
||||
return bounds[0];
|
||||
} else if (handleId) {
|
||||
return bounds.find((d) => d.id === handleId) || null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './bezier-edge';
|
||||
export * from './straight-edge';
|
||||
export * from './smoothstep-edge';
|
||||
export * from './general';
|
||||
@@ -0,0 +1,195 @@
|
||||
import { getEdgeCenter } from './general';
|
||||
import { Position, type XYPosition } from '../../types';
|
||||
|
||||
export interface GetSmoothStepPathParams {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
sourcePosition?: Position;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
targetPosition?: Position;
|
||||
borderRadius?: number;
|
||||
centerX?: number;
|
||||
centerY?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
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(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2));
|
||||
|
||||
// ith this function we try to mimic a 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: Partial<XYPosition>;
|
||||
offset: number;
|
||||
}): [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 };
|
||||
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[] = [];
|
||||
let centerX, centerY;
|
||||
const [defaultCenterX, defaultCenterY, defaultOffsetX, defaultOffsetY] = getEdgeCenter({
|
||||
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: centerX, y: sourceGapped.y },
|
||||
{ x: centerX, y: targetGapped.y },
|
||||
];
|
||||
// |
|
||||
// ---
|
||||
// |
|
||||
const horizontalSplit: XYPosition[] = [
|
||||
{ x: sourceGapped.x, y: centerY },
|
||||
{ x: targetGapped.x, y: centerY },
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
centerX = points[0].x;
|
||||
centerY = points[0].y;
|
||||
}
|
||||
|
||||
const pathPoints = [source, sourceGapped, ...points, targetGapped, target];
|
||||
|
||||
return [pathPoints, centerX, centerY, defaultOffsetX, defaultOffsetY];
|
||||
}
|
||||
|
||||
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,
|
||||
sourceY,
|
||||
sourcePosition = Position.Bottom,
|
||||
targetX,
|
||||
targetY,
|
||||
targetPosition = Position.Top,
|
||||
borderRadius = 5,
|
||||
centerX,
|
||||
centerY,
|
||||
offset = 20,
|
||||
}: GetSmoothStepPathParams): [path: string, labelX: number, labelY: number, offsetX: number, offsetY: number] {
|
||||
const [points, labelX, labelY, offsetX, offsetY] = getPoints({
|
||||
source: { x: sourceX, y: sourceY },
|
||||
sourcePosition,
|
||||
target: { x: targetX, y: targetY },
|
||||
targetPosition,
|
||||
center: { x: centerX, y: centerY },
|
||||
offset,
|
||||
});
|
||||
|
||||
const path = points.reduce<string>((res, p, i) => {
|
||||
let segment = '';
|
||||
|
||||
if (i > 0 && i < points.length - 1) {
|
||||
segment = getBend(points[i - 1], p, points[i + 1], borderRadius);
|
||||
} else {
|
||||
segment = `${i === 0 ? 'M' : 'L'}${p.x} ${p.y}`;
|
||||
}
|
||||
|
||||
res += segment;
|
||||
|
||||
return res;
|
||||
}, '');
|
||||
|
||||
return [path, labelX, labelY, offsetX, offsetY];
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { getEdgeCenter } from './general';
|
||||
|
||||
export type GetStraightPathParams = {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
};
|
||||
|
||||
export function getStraightPath({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
}: GetStraightPathParams): [path: string, labelX: number, labelY: number, offsetX: number, offsetY: number] {
|
||||
const [labelX, labelY, offsetX, offsetY] = getEdgeCenter({
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
});
|
||||
|
||||
return [`M ${sourceX},${sourceY}L ${targetX},${targetY}`, labelX, labelY, offsetX, offsetY];
|
||||
}
|
||||
@@ -0,0 +1,427 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import {
|
||||
boxToRect,
|
||||
clamp,
|
||||
clampPosition,
|
||||
devWarn,
|
||||
getBoundsOfBoxes,
|
||||
getOverlappingArea,
|
||||
isNumeric,
|
||||
rectToBox,
|
||||
} from './utils';
|
||||
import {
|
||||
type Connection,
|
||||
type Transform,
|
||||
type XYPosition,
|
||||
type Rect,
|
||||
type NodeOrigin,
|
||||
type BaseNode,
|
||||
type BaseEdge,
|
||||
type FitViewParamsBase,
|
||||
type FitViewOptionsBase,
|
||||
SnapGrid,
|
||||
NodeDragItem,
|
||||
CoordinateExtent,
|
||||
OnError,
|
||||
} from '../types';
|
||||
import { errorMessages } from '../constants';
|
||||
|
||||
export const isEdgeBase = <NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>(
|
||||
element: NodeType | Connection | EdgeType
|
||||
): element is EdgeType => 'id' in element && 'source' in element && 'target' in element;
|
||||
|
||||
export const isNodeBase = <NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>(
|
||||
element: NodeType | Connection | EdgeType
|
||||
): element is NodeType => 'id' in element && !('source' in element) && !('target' in element);
|
||||
|
||||
export const getOutgoersBase = <NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>(
|
||||
node: NodeType,
|
||||
nodes: NodeType[],
|
||||
edges: EdgeType[]
|
||||
): NodeType[] => {
|
||||
if (!isNodeBase(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const outgoerIds = edges.filter((e) => e.source === node.id).map((e) => e.target);
|
||||
return nodes.filter((n) => outgoerIds.includes(n.id));
|
||||
};
|
||||
|
||||
export const getIncomersBase = <NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>(
|
||||
node: NodeType,
|
||||
nodes: NodeType[],
|
||||
edges: EdgeType[]
|
||||
): NodeType[] => {
|
||||
if (!isNodeBase(node)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const incomersIds = edges.filter((e) => e.target === node.id).map((e) => e.source);
|
||||
return nodes.filter((n) => incomersIds.includes(n.id));
|
||||
};
|
||||
|
||||
const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection | BaseEdge): string =>
|
||||
`reactflow__edge-${source}${sourceHandle || ''}-${target}${targetHandle || ''}`;
|
||||
|
||||
const connectionExists = (edge: BaseEdge, edges: BaseEdge[]) => {
|
||||
return edges.some(
|
||||
(el) =>
|
||||
el.source === edge.source &&
|
||||
el.target === edge.target &&
|
||||
(el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) &&
|
||||
(el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle))
|
||||
);
|
||||
};
|
||||
|
||||
export const addEdgeBase = <EdgeType extends BaseEdge>(
|
||||
edgeParams: EdgeType | Connection,
|
||||
edges: EdgeType[]
|
||||
): EdgeType[] => {
|
||||
if (!edgeParams.source || !edgeParams.target) {
|
||||
devWarn('006', errorMessages['error006']());
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
let edge: EdgeType;
|
||||
if (isEdgeBase(edgeParams)) {
|
||||
edge = { ...edgeParams };
|
||||
} else {
|
||||
edge = {
|
||||
...edgeParams,
|
||||
id: getEdgeId(edgeParams),
|
||||
} as EdgeType;
|
||||
}
|
||||
|
||||
if (connectionExists(edge, edges)) {
|
||||
return edges;
|
||||
}
|
||||
|
||||
return edges.concat(edge);
|
||||
};
|
||||
|
||||
export type UpdateEdgeOptions = {
|
||||
shouldReplaceId?: boolean;
|
||||
};
|
||||
|
||||
export const updateEdgeBase = <EdgeType extends BaseEdge>(
|
||||
oldEdge: EdgeType,
|
||||
newConnection: Connection,
|
||||
edges: EdgeType[],
|
||||
options: UpdateEdgeOptions = { shouldReplaceId: true }
|
||||
): EdgeType[] => {
|
||||
const { id: oldEdgeId, ...rest } = oldEdge;
|
||||
|
||||
if (!newConnection.source || !newConnection.target) {
|
||||
devWarn('006', errorMessages['error006']());
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
const foundEdge = edges.find((e) => e.id === oldEdge.id) as EdgeType;
|
||||
|
||||
if (!foundEdge) {
|
||||
devWarn('007', errorMessages['error007'](oldEdgeId));
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
// Remove old edge and create the new edge with parameters of old edge.
|
||||
const edge = {
|
||||
...rest,
|
||||
id: options.shouldReplaceId ? getEdgeId(newConnection) : oldEdgeId,
|
||||
source: newConnection.source,
|
||||
target: newConnection.target,
|
||||
sourceHandle: newConnection.sourceHandle,
|
||||
targetHandle: newConnection.targetHandle,
|
||||
} as EdgeType;
|
||||
|
||||
return edges.filter((e) => e.id !== oldEdgeId).concat(edge);
|
||||
};
|
||||
|
||||
export const pointToRendererPoint = (
|
||||
{ x, y }: XYPosition,
|
||||
[tx, ty, tScale]: Transform,
|
||||
snapToGrid: boolean,
|
||||
[snapX, snapY]: [number, number]
|
||||
): XYPosition => {
|
||||
const position: XYPosition = {
|
||||
x: (x - tx) / tScale,
|
||||
y: (y - ty) / tScale,
|
||||
};
|
||||
|
||||
if (snapToGrid) {
|
||||
return {
|
||||
x: snapX * Math.round(position.x / snapX),
|
||||
y: snapY * Math.round(position.y / snapY),
|
||||
};
|
||||
}
|
||||
|
||||
return position;
|
||||
};
|
||||
|
||||
export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => {
|
||||
return {
|
||||
x: x * tScale + tx,
|
||||
y: y * tScale + ty,
|
||||
};
|
||||
};
|
||||
|
||||
export const getNodePositionWithOrigin = (
|
||||
node: BaseNode | undefined,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): XYPosition & { positionAbsolute: XYPosition } => {
|
||||
if (!node) {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
positionAbsolute: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const offsetX = (node.width ?? 0) * nodeOrigin[0];
|
||||
const offsetY = (node.height ?? 0) * nodeOrigin[1];
|
||||
|
||||
const position: XYPosition = {
|
||||
x: node.position.x - offsetX,
|
||||
y: node.position.y - offsetY,
|
||||
};
|
||||
|
||||
return {
|
||||
...position,
|
||||
positionAbsolute: node.positionAbsolute
|
||||
? {
|
||||
x: node.positionAbsolute.x - offsetX,
|
||||
y: node.positionAbsolute.y - offsetY,
|
||||
}
|
||||
: position,
|
||||
};
|
||||
};
|
||||
|
||||
export const getRectOfNodes = (nodes: BaseNode[], nodeOrigin: NodeOrigin = [0, 0]): Rect => {
|
||||
if (nodes.length === 0) {
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
}
|
||||
|
||||
const box = nodes.reduce(
|
||||
(currBox, node) => {
|
||||
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
||||
return getBoundsOfBoxes(
|
||||
currBox,
|
||||
rectToBox({
|
||||
x,
|
||||
y,
|
||||
width: node.width || 0,
|
||||
height: node.height || 0,
|
||||
})
|
||||
);
|
||||
},
|
||||
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
|
||||
);
|
||||
|
||||
return boxToRect(box);
|
||||
};
|
||||
|
||||
export const getNodesInside = <NodeType extends BaseNode>(
|
||||
nodes: NodeType[],
|
||||
rect: Rect,
|
||||
[tx, ty, tScale]: Transform = [0, 0, 1],
|
||||
partially = false,
|
||||
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
|
||||
excludeNonSelectableNodes = false,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): NodeType[] => {
|
||||
const paneRect = {
|
||||
x: (rect.x - tx) / tScale,
|
||||
y: (rect.y - ty) / tScale,
|
||||
width: rect.width / tScale,
|
||||
height: rect.height / tScale,
|
||||
};
|
||||
|
||||
const visibleNodes = nodes.reduce<NodeType[]>((res, node) => {
|
||||
const { width, height, selectable = true, hidden = false } = node;
|
||||
|
||||
if ((excludeNonSelectableNodes && !selectable) || hidden) {
|
||||
return res;
|
||||
}
|
||||
|
||||
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
||||
|
||||
const nodeRect = {
|
||||
x: positionAbsolute.x,
|
||||
y: positionAbsolute.y,
|
||||
width: width || 0,
|
||||
height: height || 0,
|
||||
};
|
||||
const overlappingArea = getOverlappingArea(paneRect, nodeRect);
|
||||
const notInitialized =
|
||||
typeof width === 'undefined' || typeof height === 'undefined' || width === null || height === null;
|
||||
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
const area = (width || 0) * (height || 0);
|
||||
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
|
||||
|
||||
if (isVisible || node.dragging) {
|
||||
res.push(node);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
|
||||
return visibleNodes;
|
||||
};
|
||||
|
||||
export const getConnectedEdgesBase = <NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>(
|
||||
nodes: NodeType[],
|
||||
edges: EdgeType[]
|
||||
): EdgeType[] => {
|
||||
const nodeIds = nodes.map((node) => node.id);
|
||||
|
||||
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target));
|
||||
};
|
||||
|
||||
export const getTransformForBounds = (
|
||||
bounds: Rect,
|
||||
width: number,
|
||||
height: number,
|
||||
minZoom: number,
|
||||
maxZoom: number,
|
||||
padding = 0.1
|
||||
): Transform => {
|
||||
const xZoom = width / (bounds.width * (1 + padding));
|
||||
const yZoom = height / (bounds.height * (1 + padding));
|
||||
const zoom = Math.min(xZoom, yZoom);
|
||||
const clampedZoom = clamp(zoom, minZoom, maxZoom);
|
||||
const boundsCenterX = bounds.x + bounds.width / 2;
|
||||
const boundsCenterY = bounds.y + bounds.height / 2;
|
||||
const x = width / 2 - boundsCenterX * clampedZoom;
|
||||
const y = height / 2 - boundsCenterY * clampedZoom;
|
||||
|
||||
return [x, y, clampedZoom];
|
||||
};
|
||||
|
||||
export function fitView<Params extends FitViewParamsBase<BaseNode>, Options extends FitViewOptionsBase<BaseNode>>(
|
||||
{ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
|
||||
options?: Options
|
||||
) {
|
||||
const filteredNodes = nodes.filter((n) => {
|
||||
const isVisible = options?.includeHiddenNodes ? n.width && n.height : !n.hidden;
|
||||
|
||||
if (options?.nodes?.length) {
|
||||
return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id);
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
});
|
||||
|
||||
const nodesInitialized = filteredNodes.every((n) => n.width && n.height);
|
||||
|
||||
if (nodes.length > 0 && nodesInitialized) {
|
||||
const bounds = getRectOfNodes(nodes, nodeOrigin);
|
||||
|
||||
const [x, y, zoom] = getTransformForBounds(
|
||||
bounds,
|
||||
width,
|
||||
height,
|
||||
options?.minZoom ?? minZoom,
|
||||
options?.maxZoom ?? maxZoom,
|
||||
options?.padding ?? 0.1
|
||||
);
|
||||
|
||||
panZoom.setViewport({ x, y, zoom }, { duration: options?.duration });
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export type GetPointerPositionParams = {
|
||||
transform: Transform;
|
||||
snapGrid?: SnapGrid;
|
||||
snapToGrid?: boolean;
|
||||
};
|
||||
|
||||
export function getPointerPosition(
|
||||
event: MouseEvent | TouchEvent,
|
||||
{ snapGrid = [0, 0], snapToGrid = false, transform }: GetPointerPositionParams
|
||||
): XYPosition & { xSnapped: number; ySnapped: number } {
|
||||
const x = 'touches' in event ? event.touches[0].clientX : event.clientX;
|
||||
const y = 'touches' in event ? event.touches[0].clientY : event.clientY;
|
||||
|
||||
const pointerPos = {
|
||||
x: (x - transform[0]) / transform[2],
|
||||
y: (y - transform[1]) / transform[2],
|
||||
};
|
||||
|
||||
// we need the snapped position in order to be able to skip unnecessary drag events
|
||||
return {
|
||||
xSnapped: snapToGrid ? snapGrid[0] * Math.round(pointerPos.x / snapGrid[0]) : pointerPos.x,
|
||||
ySnapped: snapToGrid ? snapGrid[1] * Math.round(pointerPos.y / snapGrid[1]) : pointerPos.y,
|
||||
...pointerPos,
|
||||
};
|
||||
}
|
||||
|
||||
export function calcNextPosition<NodeType extends BaseNode>(
|
||||
node: NodeDragItem | NodeType,
|
||||
nextPosition: XYPosition,
|
||||
nodes: NodeType[],
|
||||
nodeExtent?: CoordinateExtent,
|
||||
nodeOrigin: NodeOrigin = [0, 0],
|
||||
onError?: OnError
|
||||
): { position: XYPosition; positionAbsolute: XYPosition } {
|
||||
let currentExtent = node.extent || nodeExtent;
|
||||
|
||||
if (node.extent === 'parent') {
|
||||
if (node.parentNode && node.width && node.height) {
|
||||
const parent = nodes.find((n) => n.id === node.parentNode);
|
||||
const parentOrigin = parent?.origin || nodeOrigin;
|
||||
const currNodeOrigin = node.origin || nodeOrigin;
|
||||
|
||||
const { x: parentX, y: parentY } = getNodePositionWithOrigin(parent, parentOrigin).positionAbsolute;
|
||||
currentExtent =
|
||||
parent && isNumeric(parentX) && isNumeric(parentY) && isNumeric(parent.width) && isNumeric(parent.height)
|
||||
? [
|
||||
[parentX + node.width * currNodeOrigin[0], parentY + node.height * currNodeOrigin[1]],
|
||||
[
|
||||
parentX + parent.width - node.width + node.width * currNodeOrigin[0],
|
||||
parentY + parent.height - node.height + node.height * currNodeOrigin[1],
|
||||
],
|
||||
]
|
||||
: currentExtent;
|
||||
} else {
|
||||
onError?.('005', errorMessages['error005']());
|
||||
|
||||
currentExtent = nodeExtent;
|
||||
}
|
||||
} else if (node.extent && node.parentNode) {
|
||||
const parent = nodes.find((n) => n.id === node.parentNode);
|
||||
const { x: parentX, y: parentY } = getNodePositionWithOrigin(parent, parent?.origin || nodeOrigin).positionAbsolute;
|
||||
currentExtent = [
|
||||
[node.extent[0][0] + parentX, node.extent[0][1] + parentY],
|
||||
[node.extent[1][0] + parentX, node.extent[1][1] + parentY],
|
||||
];
|
||||
}
|
||||
|
||||
let parentPosition = { x: 0, y: 0 };
|
||||
|
||||
if (node.parentNode) {
|
||||
const parentNode = nodes.find((n) => n.id === node.parentNode);
|
||||
parentPosition = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin).positionAbsolute;
|
||||
}
|
||||
|
||||
const positionAbsolute = currentExtent
|
||||
? clampPosition(nextPosition, currentExtent as CoordinateExtent)
|
||||
: nextPosition;
|
||||
|
||||
return {
|
||||
position: {
|
||||
x: positionAbsolute.x - parentPosition.x,
|
||||
y: positionAbsolute.y - parentPosition.y,
|
||||
},
|
||||
positionAbsolute,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './graph';
|
||||
export * from './utils';
|
||||
export * from './marker';
|
||||
export * from './edges';
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { BaseEdge, EdgeMarker, EdgeMarkerType, MarkerProps } from '../types';
|
||||
|
||||
export function getMarkerId(marker: EdgeMarkerType | undefined, id?: string | null): string {
|
||||
if (!marker) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof marker === 'string') {
|
||||
return marker;
|
||||
}
|
||||
|
||||
const idPrefix = id ? `${id}__` : '';
|
||||
|
||||
return `${idPrefix}${Object.keys(marker)
|
||||
.sort()
|
||||
.map((key) => `${key}=${marker[key as keyof EdgeMarker]}`)
|
||||
.join('&')}`;
|
||||
}
|
||||
|
||||
export function createMarkerIds(
|
||||
edges: BaseEdge[],
|
||||
{ id, defaultColor }: { id?: string | null; defaultColor?: string }
|
||||
) {
|
||||
const ids: string[] = [];
|
||||
|
||||
return edges
|
||||
.reduce<MarkerProps[]>((markers, edge) => {
|
||||
[edge.markerStart, edge.markerEnd].forEach((marker) => {
|
||||
if (marker && typeof marker === 'object') {
|
||||
const markerId = getMarkerId(marker, id);
|
||||
if (!ids.includes(markerId)) {
|
||||
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
|
||||
ids.push(markerId);
|
||||
}
|
||||
}
|
||||
});
|
||||
return markers;
|
||||
}, [])
|
||||
.sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
import type {
|
||||
Dimensions,
|
||||
XYPosition,
|
||||
CoordinateExtent,
|
||||
Box,
|
||||
Rect,
|
||||
BaseNode,
|
||||
BaseEdge,
|
||||
NodeOrigin,
|
||||
HandleElement,
|
||||
Position,
|
||||
} from '../types';
|
||||
import { getConnectedEdgesBase } from './graph';
|
||||
|
||||
export const getDimensions = (node: HTMLDivElement): Dimensions => ({
|
||||
width: node.offsetWidth,
|
||||
height: node.offsetHeight,
|
||||
});
|
||||
|
||||
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
|
||||
|
||||
export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: CoordinateExtent) => ({
|
||||
x: clamp(position.x, extent[0][0], extent[1][0]),
|
||||
y: clamp(position.y, extent[0][1], extent[1][1]),
|
||||
});
|
||||
|
||||
// returns a number between 0 and 1 that represents the velocity of the movement
|
||||
// when the mouse is close to the edge of the canvas
|
||||
const calcAutoPanVelocity = (value: number, min: number, max: number): number => {
|
||||
if (value < min) {
|
||||
return clamp(Math.abs(value - min), 1, 50) / 50;
|
||||
} else if (value > max) {
|
||||
return -clamp(Math.abs(value - max), 1, 50) / 50;
|
||||
}
|
||||
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const calcAutoPan = (pos: XYPosition, bounds: Dimensions): number[] => {
|
||||
const xMovement = calcAutoPanVelocity(pos.x, 35, bounds.width - 35) * 20;
|
||||
const yMovement = calcAutoPanVelocity(pos.y, 35, bounds.height - 35) * 20;
|
||||
|
||||
return [xMovement, yMovement];
|
||||
};
|
||||
|
||||
export const getHostForElement = (element: HTMLElement): Document | ShadowRoot =>
|
||||
(element.getRootNode?.() as Document | ShadowRoot) || window?.document;
|
||||
|
||||
export const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({
|
||||
x: Math.min(box1.x, box2.x),
|
||||
y: Math.min(box1.y, box2.y),
|
||||
x2: Math.max(box1.x2, box2.x2),
|
||||
y2: Math.max(box1.y2, box2.y2),
|
||||
});
|
||||
|
||||
export const rectToBox = ({ x, y, width, height }: Rect): Box => ({
|
||||
x,
|
||||
y,
|
||||
x2: x + width,
|
||||
y2: y + height,
|
||||
});
|
||||
|
||||
export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
||||
x,
|
||||
y,
|
||||
width: x2 - x,
|
||||
height: y2 - y,
|
||||
});
|
||||
|
||||
export const nodeToRect = (node: BaseNode): Rect => ({
|
||||
...(node.positionAbsolute || { x: 0, y: 0 }),
|
||||
width: node.width || 0,
|
||||
height: node.height || 0,
|
||||
});
|
||||
|
||||
export const getBoundsOfRects = (rect1: Rect, rect2: Rect): Rect =>
|
||||
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)));
|
||||
|
||||
export const getOverlappingArea = (rectA: Rect, rectB: Rect): number => {
|
||||
const xOverlap = Math.max(0, Math.min(rectA.x + rectA.width, rectB.x + rectB.width) - Math.max(rectA.x, rectB.x));
|
||||
const yOverlap = Math.max(0, Math.min(rectA.y + rectA.height, rectB.y + rectB.height) - Math.max(rectA.y, rectB.y));
|
||||
|
||||
return Math.ceil(xOverlap * yOverlap);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const isRectObject = (obj: any): obj is Rect =>
|
||||
isNumeric(obj.width) && isNumeric(obj.height) && isNumeric(obj.x) && isNumeric(obj.y);
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
export const isNumeric = (n: any): n is number => !isNaN(n) && isFinite(n);
|
||||
|
||||
// used for a11y key board controls for nodes and edges
|
||||
export const elementSelectionKeys = ['Enter', ' ', 'Escape'];
|
||||
|
||||
export const devWarn = (id: string, message: string) => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
export function isInputDOMNode(event: KeyboardEvent): boolean {
|
||||
// using composed path for handling shadow dom
|
||||
const target = (event.composedPath?.()?.[0] || event.target) as HTMLElement;
|
||||
|
||||
const isInput = ['INPUT', 'SELECT', 'TEXTAREA'].includes(target?.nodeName) || target?.hasAttribute('contenteditable');
|
||||
// we want to be able to do a multi selection event if we are in an input field
|
||||
const isModifierKey = event.ctrlKey || event.metaKey || event.shiftKey;
|
||||
|
||||
// when an input field is focused we don't want to trigger deletion or movement of nodes
|
||||
return (isInput && !isModifierKey) || !!target?.closest('.nokey');
|
||||
}
|
||||
|
||||
export const isMouseEvent = (event: MouseEvent | TouchEvent): event is MouseEvent => 'clientX' in event;
|
||||
|
||||
export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRect) => {
|
||||
const isMouseTriggered = isMouseEvent(event);
|
||||
const evtX = isMouseTriggered ? event.clientX : event.touches?.[0].clientX;
|
||||
const evtY = isMouseTriggered ? event.clientY : event.touches?.[0].clientY;
|
||||
|
||||
return {
|
||||
x: evtX - (bounds?.left ?? 0),
|
||||
y: evtY - (bounds?.top ?? 0),
|
||||
};
|
||||
};
|
||||
|
||||
export const infiniteExtent: CoordinateExtent = [
|
||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||
];
|
||||
|
||||
// helper function to get arrays of nodes and edges that can be deleted
|
||||
// you can pass in a list of nodes and edges that should be deleted
|
||||
// and the function only returns elements that are deletable and also handles connected nodes and child nodes
|
||||
export function getElementsToRemove<NodeType extends BaseNode = BaseNode, EdgeType extends BaseEdge = BaseEdge>({
|
||||
nodesToRemove,
|
||||
edgesToRemove,
|
||||
nodes,
|
||||
edges,
|
||||
}: {
|
||||
nodesToRemove: Partial<NodeType>[];
|
||||
edgesToRemove: Partial<EdgeType>[];
|
||||
nodes: NodeType[];
|
||||
edges: EdgeType[];
|
||||
}): {
|
||||
matchingNodes: NodeType[];
|
||||
matchingEdges: EdgeType[];
|
||||
} {
|
||||
const nodeIds = nodesToRemove.map((node) => node.id);
|
||||
const edgeIds = edgesToRemove.map((edge) => edge.id);
|
||||
|
||||
const matchingNodes = nodes.reduce<NodeType[]>((res, node) => {
|
||||
const parentHit = !nodeIds.includes(node.id) && node.parentNode && res.find((n) => n.id === node.parentNode);
|
||||
const deletable = typeof node.deletable === 'boolean' ? node.deletable : true;
|
||||
if (deletable && (nodeIds.includes(node.id) || parentHit)) {
|
||||
res.push(node);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
const deletableEdges = edges.filter((e) => (typeof e.deletable === 'boolean' ? e.deletable : true));
|
||||
const initialHitEdges = deletableEdges.filter((e) => edgeIds.includes(e.id));
|
||||
const connectedEdges = getConnectedEdgesBase<NodeType, EdgeType>(matchingNodes, deletableEdges);
|
||||
const matchingEdges = [...initialHitEdges, ...connectedEdges];
|
||||
|
||||
return {
|
||||
matchingEdges,
|
||||
matchingNodes,
|
||||
};
|
||||
}
|
||||
|
||||
export const getPositionWithOrigin = ({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
origin = [0, 0],
|
||||
}: {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
origin?: NodeOrigin;
|
||||
}): XYPosition => {
|
||||
if (!width || !height) {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
if (origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
return {
|
||||
x: x - width * origin[0],
|
||||
y: y - height * origin[1],
|
||||
};
|
||||
};
|
||||
|
||||
export const getHandleBounds = (
|
||||
selector: string,
|
||||
nodeElement: HTMLDivElement,
|
||||
zoom: number,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): HandleElement[] | null => {
|
||||
const handles = nodeElement.querySelectorAll(selector);
|
||||
|
||||
if (!handles || !handles.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlesArray = Array.from(handles) as HTMLDivElement[];
|
||||
const nodeBounds = nodeElement.getBoundingClientRect();
|
||||
const nodeOffset = {
|
||||
x: nodeBounds.width * nodeOrigin[0],
|
||||
y: nodeBounds.height * nodeOrigin[1],
|
||||
};
|
||||
|
||||
return handlesArray.map((handle): HandleElement => {
|
||||
const handleBounds = handle.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
id: handle.getAttribute('data-handleid'),
|
||||
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
||||
x: (handleBounds.left - nodeBounds.left - nodeOffset.x) / zoom,
|
||||
y: (handleBounds.top - nodeBounds.top - nodeOffset.y) / zoom,
|
||||
...getDimensions(handle),
|
||||
};
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user