From c9ad79b5d453e8e33ab309efcac42e3726f7939b Mon Sep 17 00:00:00 2001 From: xiaoiver Date: Fri, 16 Sep 2022 15:17:47 +0800 Subject: [PATCH] fix: tolerate async function #95 (#96) * fix: tolerate async function #95 * feat: add polygon intersect test function #94 * chore: bump v3.3.0 --- README.md | 24 +++ .../unit/math/is-point-in-polygon.spec.ts | 32 ++++ .../unit/math/is-polygons-intersect.spec.ts | 55 ++++++ package.json | 2 +- src/index.ts | 1 + src/lodash/is-function.ts | 8 +- src/math/index.ts | 2 + src/math/is-point-in-polygon.ts | 51 ++++++ src/math/is-polygons-intersect.ts | 161 ++++++++++++++++++ 9 files changed, 329 insertions(+), 7 deletions(-) create mode 100644 __tests__/unit/math/is-point-in-polygon.spec.ts create mode 100644 __tests__/unit/math/is-polygons-intersect.spec.ts create mode 100644 src/math/index.ts create mode 100644 src/math/is-point-in-polygon.ts create mode 100644 src/math/is-polygons-intersect.ts diff --git a/README.md b/README.md index 42d5a6e..11a6fdb 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,30 @@ distanceSquareRoot(a: [number, number], b: [number, number]): number; const [formattedPath1, formattedPath2] = equalizeSegments(path1, path2); ``` +### isPointInPolygon + +判断一个点是否在多边形内。多边形形如: + +```js +const polygon = [ + [0, 0], + [0, 100], + [30, 100], + [30, 0], +]; + +// [0, 0] 在多边形的边上 +isPointInPolygon(polygon, 0, 0); // true +``` + +### isPolygonsIntersect + +判断两个多边形是否相交: + +```js +isPolygonsIntersect(polygon1, polygon2); +``` + ## License MIT@[AntV](https://github.com/antvis). diff --git a/__tests__/unit/math/is-point-in-polygon.spec.ts b/__tests__/unit/math/is-point-in-polygon.spec.ts new file mode 100644 index 0000000..249f0ac --- /dev/null +++ b/__tests__/unit/math/is-point-in-polygon.spec.ts @@ -0,0 +1,32 @@ +import { isPointInPolygon } from '../../../src/math'; + +describe('point in polygon', () => { + it('in polygon', () => { + const points = [ + [0, 0], + [0, 100], + [30, 100], + [30, 0], + ]; + + expect(isPointInPolygon(points, 0, 0)).toEqual(true); // 顶点 + expect(isPointInPolygon(points, 0, 10)).toEqual(true); // 边上 + expect(isPointInPolygon(points, 0, -1)).toEqual(false); // 顶点 + + expect(isPointInPolygon(points, 10, 10)).toEqual(true); // 边上 + expect(isPointInPolygon(points, 30, 0)).toEqual(true); + expect(isPointInPolygon(points, 30, 10)).toEqual(true); + }); + + it('other polygon', () => { + const points = [ + [0, 0], + [100, 0], + [0, 100], + [100, 100], + ]; + expect(isPointInPolygon(points, 50, 50)).toEqual(true); + expect(isPointInPolygon(points, 50, 40)).toEqual(true); + expect(isPointInPolygon(points, 40, 50)).toEqual(false); + }); +}); diff --git a/__tests__/unit/math/is-polygons-intersect.spec.ts b/__tests__/unit/math/is-polygons-intersect.spec.ts new file mode 100644 index 0000000..5cba6c1 --- /dev/null +++ b/__tests__/unit/math/is-polygons-intersect.spec.ts @@ -0,0 +1,55 @@ +import { isPolygonsIntersect } from '../../../src/math'; + +describe('is polygons intersect', () => { + const points = [ + [0, 0], + [0, 100], + [20, 100], + [20, 0], + ]; + + it('length < 2', () => { + expect(isPolygonsIntersect(points, [])).toEqual(false); + expect(isPolygonsIntersect([], points)).toEqual(false); + expect(isPolygonsIntersect([[0, 0]], points)).toEqual(false); + }); + + it('length = 2', () => { + const points1 = [ + [0, 0], + [20, 0], + ]; + expect(isPolygonsIntersect(points, points1)).toEqual(true); + }); + + it('no intersect', () => { + const points1 = [ + [10, 120], + [10, 120], + [30, 98], + ]; + expect(isPolygonsIntersect(points, points1)).toEqual(false); + expect(isPolygonsIntersect(points1, points)).toEqual(false); + }); + + it('intersect', () => { + const points1 = [ + [10, 120], + [10, 120], + [30, 98], + [-10, 98], + ]; + expect(isPolygonsIntersect(points, points1)).toEqual(true); + expect(isPolygonsIntersect(points1, points)).toEqual(true); + }); + + it('one in one', () => { + const points1 = [ + [1, 10], + [8, 20], + [14, 10], + ]; + expect(isPolygonsIntersect(points, points1)).toEqual(true); + expect(isPolygonsIntersect(points1, points)).toEqual(true); + }); +}); diff --git a/package.json b/package.json index 1c41fa7..3807f56 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@antv/util", - "version": "3.2.5", + "version": "3.3.0", "license": "MIT", "sideEffects": false, "main": "lib/index.js", diff --git a/src/index.ts b/src/index.ts index 74ac465..370b32c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,3 +2,4 @@ export * from './color'; export * from './matrix'; export * from './path'; export * from './lodash'; +export * from './math'; diff --git a/src/lodash/is-function.ts b/src/lodash/is-function.ts index 3f408c2..460291b 100644 --- a/src/lodash/is-function.ts +++ b/src/lodash/is-function.ts @@ -1,10 +1,6 @@ /** - * 是否为函数 - * @param {*} fn 对象 - * @return {Boolean} 是否函数 + * @see https://github.com/you-dont-need/You-Dont-Need-Lodash-Underscore#_isfunction */ -import isType from './is-type'; - export default (value: any): value is Function => { - return isType(value, 'Function'); + return typeof value === 'function'; }; diff --git a/src/math/index.ts b/src/math/index.ts new file mode 100644 index 0000000..9bd2cb4 --- /dev/null +++ b/src/math/index.ts @@ -0,0 +1,2 @@ +export { isPointInPolygon } from './is-point-in-polygon'; +export { isPolygonsIntersect } from './is-polygons-intersect'; diff --git a/src/math/is-point-in-polygon.ts b/src/math/is-point-in-polygon.ts new file mode 100644 index 0000000..5eb4539 --- /dev/null +++ b/src/math/is-point-in-polygon.ts @@ -0,0 +1,51 @@ +// 多边形的射线检测,参考:https://blog.csdn.net/WilliamSun0122/article/details/77994526 +const tolerance = 1e-6; +// 三态函数,判断两个double在eps精度下的大小关系 +function dcmp(x: number) { + if (Math.abs(x) < tolerance) { + return 0; + } + + return x < 0 ? -1 : 1; +} + +// 判断点Q是否在p1和p2的线段上 +function onSegment(p1: number[], p2: number[], q: number[]) { + if ( + (q[0] - p1[0]) * (p2[1] - p1[1]) === (p2[0] - p1[0]) * (q[1] - p1[1]) && + Math.min(p1[0], p2[0]) <= q[0] && + q[0] <= Math.max(p1[0], p2[0]) && + Math.min(p1[1], p2[1]) <= q[1] && + q[1] <= Math.max(p1[1], p2[1]) + ) { + return true; + } + return false; +} + +// 判断点P在多边形内-射线法 +export function isPointInPolygon(points: number[][], x: number, y: number) { + let isHit = false; + const n = points.length; + if (n <= 2) { + // svg 中点小于 3 个时,不显示,也无法被拾取 + return false; + } + for (let i = 0; i < n; i++) { + const p1 = points[i]; + const p2 = points[(i + 1) % n]; + if (onSegment(p1, p2, [x, y])) { + // 点在多边形一条边上 + return true; + } + // 前一个判断min(p1[1],p2[1]) 0 !== dcmp(p2[1] - y) > 0 && + dcmp(x - ((y - p1[1]) * (p1[0] - p2[0])) / (p1[1] - p2[1]) - p1[0]) < 0 + ) { + isHit = !isHit; + } + } + return isHit; +} diff --git a/src/math/is-polygons-intersect.ts b/src/math/is-polygons-intersect.ts new file mode 100644 index 0000000..e128b41 --- /dev/null +++ b/src/math/is-polygons-intersect.ts @@ -0,0 +1,161 @@ +import { isPointInPolygon } from './is-point-in-polygon'; + +type Point = { + x: number; + y: number; +}; + +type Line = { + from: Point; + to: Point; +}; + +const isBetween = (value: number, min: number, max: number) => value >= min && value <= max; +function getLineIntersect(p0: Point, p1: Point, p2: Point, p3: Point): Point | null { + const tolerance = 0.001; + const E: Point = { + x: p2.x - p0.x, + y: p2.y - p0.y, + }; + const D0: Point = { + x: p1.x - p0.x, + y: p1.y - p0.y, + }; + const D1: Point = { + x: p3.x - p2.x, + y: p3.y - p2.y, + }; + const kross: number = D0.x * D1.y - D0.y * D1.x; + const sqrKross: number = kross * kross; + const sqrLen0: number = D0.x * D0.x + D0.y * D0.y; + const sqrLen1: number = D1.x * D1.x + D1.y * D1.y; + let point: Point | null = null; + if (sqrKross > tolerance * sqrLen0 * sqrLen1) { + const s = (E.x * D1.y - E.y * D1.x) / kross; + const t = (E.x * D0.y - E.y * D0.x) / kross; + if (isBetween(s, 0, 1) && isBetween(t, 0, 1)) { + point = { + x: p0.x + s * D0.x, + y: p0.y + s * D0.y, + }; + } + } + return point; +} + +function parseToLines(points: number[][]): Line[] { + const lines = []; + const count = points.length; + for (let i = 0; i < count - 1; i++) { + const point = points[i]; + const next = points[i + 1]; + lines.push({ + from: { + x: point[0], + y: point[1], + }, + to: { + x: next[0], + y: next[1], + }, + }); + } + if (lines.length > 1) { + const first = points[0]; + const last = points[count - 1]; + lines.push({ + from: { + x: last[0], + y: last[1], + }, + to: { + x: first[0], + y: first[1], + }, + }); + } + return lines; +} + +function lineIntersectPolygon(lines: Line[], line: Line) { + let isIntersect = false; + lines.forEach((l) => { + if (getLineIntersect(l.from, l.to, line.from, line.to)) { + isIntersect = true; + return false; + } + }); + return isIntersect; +} + +type BBox = { + minX: number; + minY: number; + maxX: number; + maxY: number; +}; + +function getBBox(points: number[][]): BBox { + const xArr = points.map((p) => p[0]); + const yArr = points.map((p) => p[1]); + return { + minX: Math.min.apply(null, xArr), + maxX: Math.max.apply(null, xArr), + minY: Math.min.apply(null, yArr), + maxY: Math.max.apply(null, yArr), + }; +} + +function intersectBBox(box1: BBox, box2: BBox) { + return !(box2.minX > box1.maxX || box2.maxX < box1.minX || box2.minY > box1.maxY || box2.maxY < box1.minY); +} + +/** + * @see https://stackoverflow.com/questions/753140/how-do-i-determine-if-two-convex-polygons-intersect + */ +export function isPolygonsIntersect(points1: number[][], points2: number[][]) { + // 空数组,或者一个点返回 false + if (points1.length < 2 || points2.length < 2) { + return false; + } + + const bbox1 = getBBox(points1); + const bbox2 = getBBox(points2); + // 判定包围盒是否相交,比判定点是否在多边形内要快的多,可以筛选掉大多数情况 + if (!intersectBBox(bbox1, bbox2)) { + return false; + } + + let isIn = false; + // 判定点是否在多边形内部,一旦有一个点在另一个多边形内,则返回 + points2.forEach((point) => { + if (isPointInPolygon(points1, point[0], point[1])) { + isIn = true; + return false; + } + }); + if (isIn) { + return true; + } + // 两个多边形都需要判定 + points1.forEach((point) => { + if (isPointInPolygon(points2, point[0], point[1])) { + isIn = true; + return false; + } + }); + if (isIn) { + return true; + } + + const lines1 = parseToLines(points1); + const lines2 = parseToLines(points2); + let isIntersect = false; + lines2.forEach((line) => { + if (lineIntersectPolygon(lines1, line)) { + isIntersect = true; + return false; + } + }); + return isIntersect; +}