业务场景
公司业务需要在三维模型上进行取点,支持线段取点和区域取点功能。线段取点就是在模型上选取两个点形成一条线段,在线段内按相同的间隔取点;区域取点就是在模型上选取一个区域(不规则多边形),在多边形内部均匀的取点。
由于模型上的点可以转换成平面坐标,所以这个问题可以在canvas进行验证,在canvas中实现线段取点和区域取点功能。效果如下:(Demo在线调试)
线段取点:
区域取点:
线段取点
实现思路
线段取点很简单,使用三角函数关系就可以算出间隔点p0坐标值
- 求出起始点**(x1, y1)与终止点(x2, y2)**间的长度c,即:
c = Math.sqrt((x2-x1)² + (y2 - y1)²) - 根据取点间隔长度i,求得在线段上可取几个点n,即:
n = Math.floor(c / i) - 根据三角函数关系求出每个间隔点坐标
取点代码
/**
* 获取线段中的点
*/
function getLineSegmentPoint(lineSegment, interval) {
try {
if (interval && lineSegment && lineSegment.length === 2) {
const point1 = lineSegment[0];
const point2 = lineSegment[1];
const a = point2.y - point1.y;
const b = point2.x - point1.x;
const c = Math.sqrt(Math.pow(a, 2) + Math.pow(b, 2));
const n = Math.floor(c / interval);
const p = [];
for (let i = 1; i <= n; i++) {
const x = (b / c) * (interval * i) + point1.x;
const y = (a / c) * (interval * i) + point1.y;
p.push({ x, y });
}
return p;
} else {
console.error('线段取点失败', lineSegment, interval);
}
} catch (error) {
console.error('线段取点失败', error);
}
}
区域取点
实现思路
我们可以想一下,当一条直线穿过一个多边形时,必定会跟多边形产生一组交点,由于多边形是一个有限的闭合的图形,直线是无限延伸的,所以直线每次进入多边形必定会对应一次穿出多边形,由此我们可以得到以下结论:
对于平面内任意一条穿越多边形的直线,他进入或穿出多边形的交点总数必定是偶数,并且进入或穿出多边形的点都是连续成对出现的。
这里对进入和穿出的定义是:
进入:直线从多边形外部跨越多边形的边或者顶点进入到多边形内部;
穿出:直线从多边形内部跨越多边形的边或者顶点穿出多边形外部,这里多边形外、多边形的边都算是多边形外部
如果我们将多边形放到栅格网中,横向的栅格线会跟多边形相交形成很多交点,我们把进入或穿出多边形的交点都存起来,然后在连续的两个点之间进行栅格网取点,不就实现了在多边形内部进行均匀打点了。
特殊情况
这里还需要注意的是进入或穿出多边形的交点的定义?结合下图进行分析
- 线A与多边形的四个顶点重合了,那这四个顶点算不算是
**进入**或**穿出**多边形的交点呢,简单分析就可以得知,由于直线都没有进入到多边形内部,所以都不存在进入或穿出的情况,都不是进入或穿出多边形的交点 - 线B跟多边形有三个接触点,第一个点(点5)是直线从多边形外部进入到多边形内部,满足条件;第二个点(点6)跟直线相切,直线并没有从内部穿出到多边形外部,直线还在多边形内,不满足条件,第三个点(点7)直线从多边形内部穿出多边形外部,满足条件,所以点6不是进入或穿出多边形的交点
- 线D跟线A情况一样,直线都没有进入到多边形内部,所以都不存在进入或穿出的情况,点10不是是进入或穿出多边形的交点
- 线E跟多边形有4个接触点,重点看
点12和点13,这里多边形外、多边形的边都算是多边形外部,所以点12满足从多边形内部穿出多边形外部,点13满足是从多边形的外部进入到了多边形内部,点12和点13都算是进入或穿出多边形的交点
实现细节
具体步骤如下:
- 取多边形外接矩形,并以矩形左上角为起点,形成一个以
打点间隔为栅格间距的栅格网 - 获取每条栅格线与多边形的接触点,并判断这个点是否是进入或穿出多边形的交点(具体什么是进入或穿出多边形的交点看上面的特殊情况分析),如果是则存起来,不是则舍弃掉。如下图第二条栅格线存了[A,B,C,D]四个点。
- 第二步执行完后每一条横向栅格线都会得到一组交点,这组点个数为偶数,每两个点为一对(一个是进入多边形的交点,一个是穿出多边形的交点)
- 在每一对点之间进行取点,取点位置为栅格网的交点位置,这样可以确保取出来的点间隔是一样的
这里的难点在第二步,当横向栅格线与多边形的交点刚好是多边形的顶点时,怎么去判断这个顶点是否是进入或穿出多边形的交点,下面我们针对每种特殊情况分析具体实现,如下图线A、B、C、D、E跟多边形产生了1-10个接触点,其中1、2、3、4、6、10、12、13点都不是进入或穿出多边形的交点,需要移除掉:
- 多边形顶点6和顶点10跟线B相切,我们只要判断该点的前后两个点的Y是否在线B的同一侧就可以了
- 点1、2、3、4、12、13比较特殊,跟顶点连接的其中一条线段是水平的,这时我们需要判断该点的左右两边是否是多边形内部,如果都不是说明直线经过该点时不会进入到多边形内部,该点就不是进入或穿出多边形的交点,需要舍弃掉。
但是要怎么判断该点左右是否是多边形内部呢?
我们想一下,假设点1是多边形的起点,那么我们有两中方法去绘制多边形
- 方法一:1→2→6...→1,那么组成多边形的边(线段1→2, 2→6)的右边都是多边形内部
- 方法二:1→5>8...→1, 那么组成多边形的边(线段1→5, 5→8)的左边都是多边形内部
所以我们需要先判断每条线段的哪一边是多边形内部
```
/**
* 求出多边形内部在边的哪一边
*/
function getDirection() {
for (let i = 0; i < points.length; i++) {
const startIndex = i;
const endIndex = i + 1 >= points.length ? 0 : i + 1;
const lineStartPoint = points[startIndex];
const lineEndPoint = points[endIndex];
// 不处理平行线
if (lineStartPoint.y === lineEndPoint.y) continue;
const midPoint = {
x: (lineStartPoint.x + lineEndPoint.x) / 2,
y: (lineStartPoint.y + lineEndPoint.y) / 2,
};
const midRightPoint = {
...midPoint,
x: midPoint.x + 0.1,
};
// const midLeftPoint = {
// ...midPoint,
// x: midPoint.x - 0.1,
// }
const isRightInPoly = rayCasting(midRightPoint, points) === 'in';
// const isLeftPoint = rayCasting(midLeftPoint, points) === 'in'
// 判断该线段的方向是向下还是向上
if (lineEndPoint.y < lineStartPoint.y) {
// 向上
return isRightInPoly ? 'right' : 'left';
} // 向下
else {
return isRightInPoly ? 'left' : 'right';
}
}
}
```
然后我们在根据这个点的左右是否是多边形内部来判断改点是否要被移除。
- 情况一:p点和nextVertex是水平线,多边形边的右侧是多边形内部。当previousVertex点在P点下方时,p点左右都不是多边形内部,需要移除;当previousVertex点在P点上方时,p点左侧是多边形内部,需要保留
- 情况二:p点和nextVertex是水平线,多边形边的左侧是多边形内部。当previousVertex点在P点下方时,p点左侧是多边形内部,需要保留;当previousVertex点在P点上方时,p点左右都不是多边形内部,需要移除
- 情况三:p点和perviousVertex是水平线,多边形边的右侧是多边形内部。当nextVertex点在P点上方时,p点右侧是多边形内部,需要保留;当previousVertex点在P点下方时,p点左右都不是多边形内部,需要移除;
- 情况四:p点和nextVertex是水平线,多边形边的左侧是多边形内部。当nextVertex点在P点上方时,p点左右都不是多边形内部,需要移除;当previousVertex点在P点下方时,p点右侧是多边形内部,需要保留
3.最后一种情况,当点p的前后两条线段都是水平线时,p点左右都不是多边形内部,需要移除
/**
* 检测多边形顶点是否要去除
*/
function checkPointNeedRemove(index, direction) {
const {
currentVertex: p,
previousVertex,
nextVertex,
} = getNeighboringPoints(index);
const isTangencyPoint =
Math.sign(previousVertex.y - p.y) === Math.sign(nextVertex.y - p.y); // 前后两个点在同一侧,说明该顶点与横向栅格线相切
if (isTangencyPoint) {
return true;
}
// 改点的前后两段都是水平线
if (p.y === previousVertex.y && p.y === nextVertex.y) {
return true;
}
let pointNeedRemove = false;
// 第一段是水平线
if (p.y === previousVertex.y) {
if (p.x > previousVertex.x) {
// 线段从左往右 start ---->p
// \
// \
if (
(closingDirection === 'right' && nextVertex.y > p.y) ||
(closingDirection === 'left' && nextVertex.y < p.y)
) {
pointNeedRemove = true;
}
} else {
// 线段从右往左 p<---- start
// /
// /
if (
(closingDirection === 'right' && nextVertex.y < p.y) ||
(closingDirection === 'left' && nextVertex.y > p.y)
) {
pointNeedRemove = true;
}
}
}
// 第二段是水平线
if (p.y === nextVertex.y) {
if (p.x < nextVertex.x) {
// 线段从左往右 p----> end
// /
// /
if (
(closingDirection === 'right' && previousVertex.y > p.y) ||
(closingDirection === 'left' && previousVertex.y < p.y)
) {
pointNeedRemove = true;
}
} else {
// 线段从右往左 end <----p
// \
// \
if (
(closingDirection === 'right' && previousVertex.y < p.y) ||
(closingDirection === 'left' && previousVertex.y > p.y)
) {
pointNeedRemove = true;
}
}
}
return pointNeedRemove;
}
在线调试
完整代码在这里 DrawDemo。