refactor(edges): siplify edge path / label position handling, repair edge labels
This commit is contained in:
@@ -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}`;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user