import type { AABB } from '@antv/g'; import { isEqual } from '@antv/util'; import type { Point, PointObject } from '../types'; import { getBBoxHeight, getBBoxWidth } from './bbox'; import type { LineSegment } from './line'; import { getLinesIntersection, isLinesParallel } from './line'; import { getXYByPlacement } from './position'; import { add, distance, divide, normalize, subtract, toVector2 } from './vector'; /** * 将对象坐标转换为数组坐标 * Convert object coordinates to array coordinates * @param point - 对象坐标 | object coordinates * @returns 数组坐标 | array coordinates */ export function parsePoint(point: PointObject): Point { return [point.x, point.y, point.z ?? 0]; } /** * 将数组坐标转换为对象坐标 * * Convert array coordinates to object coordinates * @param point - 数组坐标 | array coordinates * @returns 对象坐标 | object coordinates */ export function toPointObject(point: Point): PointObject { return { x: point[0], y: point[1], z: point[2] ?? 0 }; } /** * 根据 X 轴坐标排序 * Sort by X-axis coordinates * @param points - 点集 | point set * @returns 排序后的点集 | sorted point set */ export function sortByX(points: Point[]): Point[] { return points.sort((a, b) => a[0] - b[0] || a[1] - b[1]); } /** * 点集去重 * * Deduplicate point set * @param points - 点集 | pointset * @returns 去重后的点集 | deduplicated pointset */ export function deduplicate(points: Point[]): Point[] { const set = new Set(); return points.filter((p) => { const key = p.join(','); if (set.has(key)) return false; set.add(key); return true; }); } /** * 对点格式化,精确到 `digits` 位的数字 * * Round the point to the given precision * @param point - 要舍入的点 | the point to round * @param digits - 小数点后的位数 | the number of digits after the decimal point * @returns 舍入后的点 | the rounded point */ export function round(point: Point, digits = 0): Point { return point.map((p) => parseFloat(p.toFixed(digits))) as Point; } /** * 移动点,将点朝向参考点移动一定的距离 * * Move `p` point along the line starting from `ref` to this point by a certain `distance` * @param p - 要移动的点 | the point to move * @param ref - 参考点 | the reference point * @param distance - 移动的距离 | the distance to move * @param reverse * @returns 移动后的点 | the moved point */ export function moveTo(p: Point, ref: Point, distance: number, reverse = false): Point { if (isEqual(p, ref)) return p; const direction = reverse ? subtract(p, ref) : subtract(ref, p); const normalizedDirection = normalize(direction); const moveVector: Point = [normalizedDirection[0] * distance, normalizedDirection[1] * distance]; return add(toVector2(p), moveVector); } /** * 判断两个点是否在同一水平线上 * * whether two points are on the same horizontal line * @param p1 - 第一个点 | the first point * @param p2 - 第二个点 | the second point * @returns 返回两个点是否在同一水平线上 | is horizontal or not */ export function isHorizontal(p1: Point, p2: Point): boolean { return p1[1] === p2[1]; } /** * 判断两个点是否在同一垂直线上 * * whether two points are on the same vertical line * @param p1 - 第一个点 | the first point * @param p2 - 第二个点 | the second point * @returns 返回两个点是否在同一垂直线上 | is vertical or not */ export function isVertical(p1: Point, p2: Point): boolean { return p1[0] === p2[0]; } /** * 判断两个点是否正交,即是否在同一水平线或垂直线上 * * Judges whether two points are orthogonal, that is, whether they are on the same horizontal or vertical line * @param p1 - 第一个点 | the first point * @param p2 - 第二个点 | the second point * @returns 是否正交 | whether orthogonal or not */ export function isOrthogonal(p1: Point, p2: Point): boolean { return isHorizontal(p1, p2) || isVertical(p1, p2); } /** * 判断是否三点共线 * * Judge whether three points are collinear * @param p1 - 第一个点 | the first point * @param p2 - 第二个点 | the second point * @param p3 - 第三个点 | the third point * @returns 是否三点共线 | whether three points are collinear */ export function isCollinear(p1: Point, p2: Point, p3: Point): boolean { return isLinesParallel([p1, p2], [p2, p3]); } /** * 计算一个点相对于另一个点的中心对称点 * * Calculate the center symmetric point of a point relative to another point * @param p - 要计算的点 | the point to calculate * @param center - 中心点 | the center point * @returns 中心对称点 | the center symmetric point */ export function getSymmetricPoint(p: Point, center: Point): Point { return [2 * center[0] - p[0], 2 * center[1] - p[1]]; } /** * 获取从多边形中心到给定点的连线与多边形边缘的交点 * * Gets the intersection point between the line from the center of a polygon to a given point and the polygon's edge * @param p - 从多边形中心到多边形边缘的连线的外部点 | The point outside the polygon from which the line to the polygon's center is drawn * @param center - 多边形中心 | the center of the polygon * @param points - 多边形顶点 | the vertices of the polygon * @param isRelativePos - 顶点坐标是否相对中心点 | whether the vertex coordinates are relative to the center point * @param useExtendedLine - 是否使用延长线 | whether to use the extended line * @returns 交点与相交线段 | intersection and intersecting line segment */ export function getPolygonIntersectPoint( p: Point, center: Point, points: Point[], isRelativePos = true, useExtendedLine = false, ): { point: Point; line?: LineSegment } { for (let i = 0; i < points.length; i++) { let start = points[i]; let end = points[(i + 1) % points.length]; if (isRelativePos) { start = add(center, start); end = add(center, end); } const refP = useExtendedLine ? getSymmetricPoint(p, center) : p; const intersect = getLinesIntersection([center, refP], [start, end]); if (intersect) { return { point: intersect, line: [start, end], }; } } return { point: center, line: undefined, }; } /** * 判断点是否在多边形内部 * * Whether point is inside the polygon (ray algo) * @param point - 点 | point * @param points - 多边形顶点 | polygon vertices * @param start - 起始索引 | start index * @param end - 结束索引 | end index * @returns 是否在多边形内部 | whether inside the polygon */ export function isPointInPolygon(point: Point, points: Point[], start?: number, end?: number): boolean { const x = point[0]; const y = point[1]; let inside = false; if (start === undefined) start = 0; if (end === undefined) end = points.length; const len = end - start; for (let i = 0, j = len - 1; i < len; j = i++) { const xi = points[i + start][0]; const yi = points[i + start][1]; const xj = points[j + start][0]; const yj = points[j + start][1]; const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; if (intersect) inside = !inside; } return inside; } /** * 获取给定点到矩形中心的连线与矩形边缘的交点 * * Gets the intersection point between the line from the center of a rectangle to a given point and the rectangle's edge * @param p - 从矩形中心到矩形边缘的连线的外部点 | The point outside the rectangle from which the line to the rectangle's center is drawn * @param bbox - 矩形包围盒 | the bounding box of the rectangle * @param useExtendedLine - 是否使用延长线 | whether to use the extended line * @returns 交点 | intersection */ export function getRectIntersectPoint(p: Point, bbox: AABB, useExtendedLine = false): Point { const center = getXYByPlacement(bbox, 'center'); const corners = [ getXYByPlacement(bbox, 'left-top'), getXYByPlacement(bbox, 'right-top'), getXYByPlacement(bbox, 'right-bottom'), getXYByPlacement(bbox, 'left-bottom'), ]; return getPolygonIntersectPoint(p, center, corners, false, useExtendedLine).point; } /** * 获取给定点到椭圆中心的连线与椭圆边缘的交点 * * Gets the intersection point between the line from the center of an ellipse to a given point and the ellipse's edge * @param p - 从椭圆中心到椭圆边缘的连线的外部点 | The point outside the ellipse from which the line to the ellipse's center is drawn * The point outside the ellipse from which the line to the ellipse's center is drawn. * @param bbox - 椭圆包围盒 | the bounding box of the ellipse * @param useExtendedLine - 是否使用延长线 | whether to use the extended line * @returns 交点 | intersection */ export function getEllipseIntersectPoint(p: Point, bbox: AABB, useExtendedLine = false): Point { const center = bbox.center; const refP = useExtendedLine ? getSymmetricPoint(p, center) : p; const vec = subtract(refP, bbox.center); const angle = Math.atan2(vec[1], vec[0]); if (isNaN(angle)) return center; const rx = getBBoxWidth(bbox) / 2; const ry = getBBoxHeight(bbox) / 2; const intersectX = center[0] + rx * Math.cos(angle); const intersectY = center[1] + ry * Math.sin(angle); return [intersectX, intersectY]; } /** * 从两组点中找到距离最近的两个点 * @param group1 - 第一组点 | the first group of points * @param group2 - 第二组点 | the second group of points * @returns 距离最近的两个点 | the nearest two points */ export function findNearestPoints(group1: Point[], group2: Point[]): [Point, Point] { let minDistance = Infinity; let nearestPoints: [Point, Point] = [group1[0], group2[0]]; group1.forEach((p1) => { group2.forEach((p2) => { const dist = distance(p1, p2); if (dist < minDistance) { minDistance = dist; nearestPoints = [p1, p2]; } }); }); return nearestPoints; } /** * 从一组线段中找到距离给定点最近的线段 * * Find the line segment closest to the given point from a group of line segments * @param point - 给定点 | the given point * @param lines - 一组线段 | a group of line segments * @returns 距离最近的线段 | the nearest line segment */ export function findNearestLine(point: Point, lines: LineSegment[]) { let minDistance = Infinity; let nearestLine: [Point, Point] = [ [0, 0], [0, 0], ]; lines.forEach((line) => { const distance = getDistanceToLine(point, line); if (distance < minDistance) { minDistance = distance; nearestLine = line; } }); return nearestLine; } /** * 获取点到线段的距离 * * Get the distance from a point to a line segment * @param point - 点 | the point * @param line - 线段 | the line segment * @returns 点到线段的距离 | the distance from the point to the line segment */ export function getDistanceToLine(point: Point, line: LineSegment) { const nearestPoint = findNearestPointOnLine(point, line); return distance(point, nearestPoint); } /** * 获取线段上距离给定点最近的点 * * Get the point on the line segment closest to the given point * @param point - 给定点 | the given point * @param line - 线段 | the line segment * @returns 线段上距离给定点最近的点 | the point on the line segment closest to the given point */ export function findNearestPointOnLine(point: Point, line: LineSegment): Point { const [x1, y1] = line[0]; const [x2, y2] = line[1]; const [x3, y3] = point; const px = x2 - x1; const py = y2 - y1; // 若线段实际上是一个点 | If the line segment is actually a point if (px === 0 && py === 0) { return [x1, y1]; } let u = ((x3 - x1) * px + (y3 - y1) * py) / (px * px + py * py); if (u > 1) { u = 1; } else if (u < 0) { u = 0; } const x = x1 + u * px; const y = y1 + u * py; return [x, y]; } /** * 获取点集的中心点 * * Get the center point of a set of points * @param points - 点集 | point set * @returns 中心点 | center point */ export function centerOf(points: Point[]): Point { const totalPosition = points.reduce((acc, p) => add(acc, p), [0, 0]); return divide(totalPosition, points.length); } /** * 按顺时针或逆时针方向对点集排序 * * Sort the point set in a clockwise or counterclockwise direction * @param points - 点集 | point set * @param clockwise - 是否顺时针 | whether clockwise * @returns 排序后的点集 | sorted point set */ export function sortByClockwise(points: Point[], clockwise = true): Point[] { const center = centerOf(points); return points.sort(([x1, y1], [x2, y2]) => { const angle1 = Math.atan2(y1 - center[1], x1 - center[0]); const angle2 = Math.atan2(y2 - center[1], x2 - center[0]); return clockwise ? angle2 - angle1 : angle1 - angle2; }); } /** * 给定的起点和终点,返回一个由这两个点和它们的对角点组成的数组 * @param start - 起点 | start point * @param end - 终点 | end point * @returns 由这两个点和它们的对角点组成的数组 | an array consisting of these two points and their diagonal points */ export function getBoundingPoints(start: Point, end: Point): Point[] { return [start, [start[0], end[1]], end, [end[0], start[1]]]; }