需求目标
需要在模型移动过程中感知到模型一个“十”字形的范围内的所有模型,如有相交则标注相隔距离,如果没有感知到阻挡模型,则显示到墙体的距离。
方案思路
一开始,当我拿到这个需求的时候,我的第一想法就是在模型的周围做四条射线,如果和其他模型有相交点,则以相交点的位置 做连线。
下一秒就产生了一个问题,如图。
在这样的情况下,标注线应该标注最近的那一个模型,但是却标注到了另外一个模型。
但有一点是母庸质疑的,那就是他需要有四条标注线,要分开来一条一条处理。
接着,拿其中一条边来说,是否应该要找到最近的一个模型?
这两种情况都证明了上面这种思路的问题所在,要考虑方向和模型大小。
那么是否能够在保证当前模型四条边绘制顺序的情况下,寻找右侧的所有模型的 boundingBox 的 4 个点的集合,然后在取集合中里这条边最近的点?
首先解释一下,什么是 boundingBox,当在模型计算的时候,经常会遇到一些多边形,或者模型旋转,为了简化处理,我们将多边形放在一个最小正矩形内,而这个最小正矩形就是 boundingBox。如下图:
如果按照上面 的思路,在不考虑性能的情况下,能否满足要求呢,思考了一下反例又来了。如图:
在上图中,B 在 A 的右侧最近,应该被有标注。但按照之前的算法,B 的 4 个点并不在 A 这一边的感应区域内,所以不会做处理。
所以,按照点的算法来说是不能处理的。如果要用单条线去比较,那么必须用线与线的比较方式来计算。
如果使用线段来算,大概思路有一下内容:
- 找到当前选中模型的boundingBox2D的四条边
- 遍历四条边,查找每一条边的最近模型包含
- 找到所有模型(可过滤为当前房间的所有模型)
- 遍历所有模型boundingBox2D
- 遍历boundingBox2D的四条线段
- 是与比较线段平行?
- 是在比较线段的感应范围内?
- 横向线段比较x范围
- 纵向线段比较y范围
- .........
当思路 理到这里的时候,我就已经无法继续下去了,这样的算法显然不是我想要的 ,并且上面的算法,并没有包含房间 墙体的内容,即使这样实现了,也肯定存在其他遗漏掉的内容。
既然线段实现过程还是有很大困难,那么回到 看原型图。发现,按照需求,是需要感知模型 周围四个方向的矩形区域。那么是否应该使用 Polygon 多边形去处理更好呢。
使用多边形处理,首先要解决的内容是:如何判断两个多边形相交?
显然这样的算法是普遍的,这里不做具体展开,所以在封装了Polygon2D.insterectPolygon(polygon:Polygon2D):Polygon2D
这样的方法后,再理思路,下方附有过程图:
- 找到选中模型的boundingBox2D的四个射线感应多边形(扩大多边形范围)(图1)
- 获取所有需要查找的范围集合,包含模型和墙体(后面统一称为compares)(图2)
- 遍历四个方向的感应多边形
- 遍历查找范围集合,从compares中查找到最近的一点
- 过滤未相交的compare(图3)
- 将所有过滤后的polygon集合展开为所有多边形点的集合=>points
- 找出points与选中模型最近的点(图4)
- 找到的最近一点与当前方向一边的垂足
- 连接垂足与最近点成线段(图5)
- 平移线段到正确点的位置(图6)
- 绘制找到的四条线段
- 后续对线段长度标注、根据线段方向与设置大小,改变模型位置...
过程图:
图 1
图 2
图 3
图 4
图 5
图 6
最终效果
代码实现
/**
* 根据模型获取 模型四个方向的最近射线
* @param srcModel
* @return {Line2D[]}
* Created by yee.wang on 2018/9/7
*/
public static getNearLines(srcModel: ModelDataBase): Line2D[] {
const room = ToolRoom.getRoomByPoint(
srcModel.position,
Scene3D.getInstance().homePlan.roomLayer.getDatas() as Room[],
);
if (!room) {
return [];
}
// 当前模型的4个边界点
const edgePoints: Vector2D[] = (() => {
const modelBoundingPoints = ToolModel.getModelPoints(srcModel);
return [
// top:
minBy(modelBoundingPoints, vec => vec.y).clone(),
// right:
maxBy(modelBoundingPoints, vec => vec.x).clone(),
// bottom:
maxBy(modelBoundingPoints, vec => vec.y).clone(),
// left:
minBy(modelBoundingPoints, vec => vec.x).clone(),
];
})();
// 根据当前模型获取到的感应区域
const inductionPolygons = (() => {
const boundingBox = new BoundingBox2D();
edgePoints.forEach(point => boundingBox.expandByPoint(point));
return boundingBox.polygon.getEdges().map(line => {
line = line.translateLeft(1);
line.setLength(line.length - 1); // 缩小范围,防止边界重叠
const rayLine = line.translateLeft(2000);
return new Polygon2D([line.start, line.end, rayLine.end, rayLine.start]);
});
})();
const position2D = new Vector2D(srcModel.position.x, srcModel.position.z);
// 当前房间的除了当前模型的所有模型
const models = room.getModelsInRoom() as ModelDataBase[];
const cubeBoxes = room
.getCubeBoxesInRoom()
.map(cubeBox => ToolCubeBox.makeVirtualModel(cubeBox)) as ModelDataBase[];
const walls = room.walls as Wall[];
const srcPolygon = srcModel.getPolygon();
const nearLines: Line2D[] = [];
inductionPolygons.forEach((polygon, index) => {
const modelLine = new Line2D(polygon.vertices[0], polygon.vertices[1]);
const rayLine = modelLine.setLength(2000);
const intersectResults = [...models, ...walls, ...cubeBoxes].filter(compare => {
let pos2D: Vector2D = null;
if (compare.position instanceof Vector3D) {
pos2D = new Vector2D(compare.position.x, compare.position.z);
} else {
pos2D = compare.position.clone();
}
// 过滤在线段右侧的物体
if (rayLine.isRight(pos2D)) {
return false;
}
const comparePolygon = compare.getPolygon();
// 过滤与自身相交的模型
if (!!srcPolygon.intersectPolygon(comparePolygon.boundingBox.polygon)) {
return false;
}
return !!polygon.intersectPolygon(comparePolygon);
});
const allPoints = intersectResults.reduce((prev, next) => prev.concat(next.getPolygon().vertices), []);
const nearestPoint = minBy(allPoints, point => position2D.distanceSquared(point));
if (nearestPoint) {
const footPoint = nearestPoint.footPoint(rayLine);
const resultLine = new Line2D(footPoint, nearestPoint);
const originPoint = edgePoints[index];
resultLine.translateBy(originPoint.subtract(resultLine.start) as Vector2D);
nearLines.push(resultLine);
}
});
return nearLines;
}