Files
xyflow/packages/system/src/utils/general.ts
T

178 lines
5.0 KiB
TypeScript

import type {
Dimensions,
XYPosition,
CoordinateExtent,
Box,
Rect,
NodeBase,
NodeOrigin,
SnapGrid,
Transform,
} from '../types';
import { getNodePositionWithOrigin } from './graph';
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 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: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return {
...positionAbsolute,
width: node.width || 0,
height: node.height || 0,
};
};
export const nodeToBox = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
return {
...positionAbsolute,
x2: positionAbsolute.x + (node.width || 0),
y2: positionAbsolute.y + (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 devWarn = (id: string, message: string) => {
if (process.env.NODE_ENV === 'development') {
console.warn(`[React Flow]: ${message} Help: https://reactflow.dev/error#${id}`);
}
};
export const getPositionWithOrigin = ({
x,
y,
width,
height,
origin = [0, 0],
}: {
x: number;
y: number;
width: number;
height: number;
origin?: NodeOrigin;
}): XYPosition => {
if (!width || !height || 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 function snapPosition(position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition {
return {
x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
y: snapGrid[1] * Math.round(position.y / snapGrid[1]),
};
}
export const pointToRendererPoint = (
{ x, y }: XYPosition,
[tx, ty, tScale]: Transform,
snapToGrid = false,
snapGrid: SnapGrid = [1, 1]
): XYPosition => {
const position: XYPosition = {
x: (x - tx) / tScale,
y: (y - ty) / tScale,
};
return snapToGrid ? snapPosition(position, snapGrid) : position;
};
export const rendererPointToPoint = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => {
return {
x: x * tScale + tx,
y: y * tScale + ty,
};
};
export const getTransformForBounds = (
bounds: Rect,
width: number,
height: number,
minZoom: number,
maxZoom: number,
padding: number
): 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 const isMacOs = () => typeof navigator !== 'undefined' && navigator?.userAgent?.indexOf('Mac') >= 0;