BIMFACE中矩形空间拆分与合并
应用场景
在BIM
运维场景中,空间同设备一样,作为一种资产被纳入运维管理体系,典型的应用场景例如商铺、防火分区等,这就涉及到空间的拆分和合并,在bimface
中,已经实现了空间的动态调整,但是距离自定义的,较为直观的空间拆分与合并,目前的处理方式还不能够满足业务场景的需求,于是自行完成了基于bimface
的矩形空间的拆分与合并的实现过程。
先说合并
合并矩形空间的前提条件,是有两个及以上的且相邻的矩形空间,如果两个空间不相邻,也就失去了合并的意义,即使合并也不能够表达出真实物理世界的空间结构。空间合并相对来说比较简单,每个空间都是有一系列的有序的点围起来的二维封闭平面,这一系列的点集暂且称之为边界信息点集,加上高度参数就形成了三维立体空间。相邻的矩形空间必然会有近似重合的点,如下图的黄圈部分,如果把这些点去掉,只保留最外围的点(极值点,如下图的白圈部分),就形成了一个新的有序的点集,构成了新的边界信息,再加上合理的高度,被合并的空间就产生了。
以下是空间合并的核心代码:
/**
* 空间合并处理管道,适用于多个规则且相邻的矩形空间
* @param {Array} boundaryArray 空间边界数据数组(必填)
* @param {String} id 空间唯一标识(非必填)
* @param {Number} height 空间高度(非必填)
* @param {Glodon.Web.Graphics.Color} faceColor 空间表面颜色(非必填)
* @param {Glodon.Web.Graphics.Color} frameColor 空间轮廓颜色(非必填)
* @returns {Object} 新构造的空间边界
* @requires WebUtils
* @public
*/
mergeBoundaryPipeline: function (boundaryArray, id, height, faceColor, frameColor) {
if (!boundaryArray || !boundaryArray.length) {
console.warn("boundaryArray is empty!");
return;
}
const vertical = 1;
for (let n = 0, len = boundaryArray.length; n < len; n++) {
//第一步:整理数据,去除小数部分
let cleanData = this.cleanBoundaryData(boundaryArray[n]);
//第二步:将所有的点数据存储至一维数组
this.storePointArray(cleanData);
}
//第三步:筛选极值点
let extremum = this.extremumBoundaryPoint(this.pointCollection, vertical);
//第四步:根据极值点构造新边界
let newBoundary = this.buildBoundary(extremum);
this.viewer.createRoom(newBoundary, height || 5500, id || webUtils.guid(), faceColor || webUtils.fromHexColor('#ff0000', 0.25), frameColor || webUtils.fromHexColor('#ff0000'));
return newBoundary;
},
/**
* 通过顶点集合获取极值点,以便构造新的空间边界
* @param {Array} pointCollection 被合并前的多个空间的顶点集合
* @param {Number} direction 原空间的分隔方向 1:纵向 2:横向
* @returns {Array} 从一系列顶点中筛选出的顶点集合
*/
extremumBoundaryPoint: function (pointCollection, direction) {
const vertical = 1, horizontal = 2;
let extremumPoint = [];
minX = maxX = pointCollection[0].x;
minY = maxY = pointCollection[0].y;
for (let n = 1, len = pointCollection.length; n < len; n++) {
pointCollection[n].x > maxX ? maxX = pointCollection[n].x : null;
pointCollection[n].x < minX ? minX = pointCollection[n].x : null;
pointCollection[n].y > maxY ? maxY = pointCollection[n].y : null;
pointCollection[n].y < minY ? minY = pointCollection[n].y : null;
}
for (let k = 0, len = pointCollection.length; k < len; k++) {
let currentPoint = pointCollection[k];
if (direction === 1) {
if (!(currentPoint.x > minX && currentPoint.x < maxX)) {
let exist = extremumPoint.some(item => {
if (item.x == currentPoint.x && item.y == currentPoint.y) {
return true;
}
return false;
})
if (!exist) {
extremumPoint.push(currentPoint);
}
} else {
// console.log("分割方向:纵向");
}
}
if (direction === 2) {
if (!(currentPoint.y > minY && currentPoint.y < maxY)) {
let exist = extremumPoint.some(item => {
if (item.x == currentPoint.x && item.y == currentPoint.y) {
return true;
}
return false;
})
if (!exist) {
extremumPoint.push(currentPoint);
}
}
}
}
//对符合条件的点集进行顺时针排序,思路是找到最大和最小占1、3索引,剩余的两个点随机
return extremumPoint;
}
蓝色代表原始的分离的空间,红色代表合并后的空间效果
再说拆分
空间的拆分相对于合并就比较麻烦,因为合并只有一种方式,单拆分却有很多种。例如,沿着相对于空间水平方向或者垂直方向切割、沿着对角线切割、斜方向切割等,要考虑多种可能性。大体的思路是,首先监听鼠标单击事件,获取单击的两个点位置作为参数,可以计算出过该两点的直线,有了直线方程,再分别与空间边界的四条边计算交点,如果交点不在边界信息围成的区域内则丢弃,只保留在边界信息内的交点,如果与矩形区域相交,必然是两个交点(与矩形顶点相交没有意义,排除一个交点的可能),再按照拆分的类型分别计算拆分后的点集并排序,计算出两个新的边界点集,最终绘制出两个新的空间。
空间拆分的核心算法如下:
/**
* 根据二维坐标点集和求解二元一次方程直线
* @param {Array} pointArray 二维坐标点集合 [{x:100,y:200},{x:200,y:400}]
* @returns {Object} 返回直线【Y = Ax + b】的斜率【A】和截距【b】
*/
resolveEquation: function (pointArray) {
let result = {
A: 0, b: 0
};
if (!pointArray || !pointArray.length) {
console.warn("parameter pointArray invalidate!");
return;
}
//解方程 Y = Ax + b 核心算法,此处考虑要不要四舍五入
let A, b
//不存在斜率
if (Math.round(pointArray[1].y) === Math.round(pointArray[0].y)) {
A = 0;
b = pointArray[0].y;
console.log("点集" + JSON.stringify(pointArray) + "对应的二元一次方程为:Y = " + b);
} else if (Math.round(pointArray[0].x) === Math.round(pointArray[1].x)) {
A = 0;
b = pointArray[0].x;
console.log("点集" + JSON.stringify(pointArray) + "对应的二元一次方程为:X = " + b);
}
//存在斜率
else {
A = (pointArray[1].y - pointArray[0].y) / (pointArray[1].x - pointArray[0].x);
b = pointArray[0].y - pointArray[0].x * (pointArray[0].y - pointArray[1].y) / (pointArray[0].x - pointArray[1].x);
console.log("点集" + JSON.stringify(pointArray) + "对应的二元一次方程为:Y = " + A + "*x + " + b);
}
result.A = A;
result.b = b;
return result;
},
/**
* 根据点集合与边界计算交点
* @param {Object} boundary 空间边界数据
* @param {Array} pointArray 分割点集合
* @param {Number} height 高度
* @requires RoomUtils
* @returns {Array} crossPointArray 直线与边界交点集合
*/
findCrossPoint: function (boundary, pointArray, height) {
let roomUtils = new RoomUtils();
//整理边界数据
boundary = roomUtils.cleanBoundaryData(boundary);
//计算分割点集所在的直线方程 Y = Ax + b
let { A, b } = this.resolveEquation(pointArray);
let pointList = boundary.loops[0];
//直线与边界的交点集合,N条边N个点,最终会保留两个交点
let pointCollection = [];
let crossObjectArray = [];
for (let n = 0, len = pointList.length; n < len; n++) {
//item => 标识线段的两端点集合 [{x:x,y:y},{x:x,y:y}]
let item = pointList[n];
let roundX0 = Math.round(item[0].x), roundX1 = Math.round(item[1].x);
let roundY0 = Math.round(item[0].y), roundY1 = Math.round(item[1].y);
let crossObject = { item: item, cross: false, crossBy: undefined };
//当边界线是垂直直线
if (roundX0 === roundX1) {
let y = this.calculateCoordinate(A, b, item[0].x, 0);
let point = { x: item[0].x, y: y, z: height };
//如果交点Y坐标在线段两端之间则加入到集合
if (Math.min(item[0].y, item[1].y) < y && Math.max(item[0].y, item[1].y) > y) {
pointCollection.push(new THREE.Vector3(point.x, point.y, point.z));
crossObject.cross = true;
crossObject.crossBy = new THREE.Vector3(point.x, point.y, point.z);
}
}
//当边界线是水平直线
if (roundY0 === roundY1) {
let x = this.calculateCoordinate(A, b, 0, item[0].y);
let point = { x: x, y: item[0].y, z: height };
//如果交点X坐标在线段两端之间则加入到集合
if (Math.min(item[0].x, item[1].x) < x && Math.max(item[0].x, item[1].x) > x) {
pointCollection.push(new THREE.Vector3(point.x, point.y, point.z));
crossObject.cross = true;
crossObject.crossBy = new THREE.Vector3(point.x, point.y, point.z);
}
}
crossObjectArray.push(crossObject);
//其他情形暂不考虑,先验证可行性与准确性
}
return { pointCollection: pointCollection, crossObjectArray: crossObjectArray };
},
/**
* 创建拆分后的空间
* @param {Array} crossObjectArray 用于拆分空间的点集合
* @requires WebUtils
* @requires ModelHelper
* @returns {Array} 拆分后的空间边界集合
*/
buildSplitAreas: function (crossObjectArray) {
if (!crossObjectArray) return;
console.log(crossObjectArray);
var webUtils = new WebUtils();
var modelHelper = new ModelHelper();
//标识切割边是否相邻
let isAdjacent = false;
let boundaryCollection = [];
//区分邻边还是对边
for (let i = 0, len = crossObjectArray.length; i < len; i++) {
if (i !== len - 1 && crossObjectArray[i].cross && crossObjectArray[i + 1].cross) {
isAdjacent = true;
}
};
//首尾相接时
if (crossObjectArray[0].cross && crossObjectArray[crossObjectArray.length - 1].cross) {
isAdjacent = true;
}
console.log(isAdjacent);
//如果交点相邻
if (isAdjacent) {
//找到切割点的公共点作为中间点构件边界
let boundaryPoints = [];
let boundary = crossObjectArray.filter(p => { return p.cross });
//找到公共点,如果不是首尾相接,取中间,否则取两边
let commonPoint = webUtils.isObjectEqual(boundary[0].item[0], boundary[1].item[1]) ? boundary[0].item[0] : boundary[0].item[1];
//寻找相交线中非公共点
let leftPoint = [];
webUtils.isObjectEqual(boundary[0].item[0], boundary[1].item[1]) ? leftPoint.push(boundary[0].item[1], boundary[1].item[0]) : leftPoint.push(boundary[0].item[0], boundary[1].item[1]);
for (let k = 0, len = boundary.length; k < len; k++) {
boundary[k].crossBy.z = 0;
boundaryPoints.push(boundary[k].crossBy);
}
boundaryPoints.splice(1, 0, commonPoint);
//获取三角侧边界对象
var boundarys = modelHelper.buildAreaBoundary(boundaryPoints);
boundaryCollection.push(boundarys);
//开始寻找另一侧点集
let oppositeBoundary = crossObjectArray.filter(p => { return !p.cross });
let oppositePoint = webUtils.isObjectEqual(oppositeBoundary[0].item[0], oppositeBoundary[1].item[1]) ? oppositeBoundary[0].item[0] : oppositeBoundary[0].item[1];
//组装另一侧空间边界
leftPoint.splice(1, 0, oppositePoint);
//点集排序
if (leftPoint[0].x === boundary[0].crossBy.x || leftPoint[0].y === boundary[0].crossBy.y) {
leftPoint.splice(0, 0, boundary[0].crossBy);
leftPoint.splice(leftPoint.length, 0, boundary[1].crossBy);
} else {
leftPoint.splice(0, 0, boundary[1].crossBy);
leftPoint.splice(leftPoint.length, 0, boundary[0].crossBy);
}
//获取非三角侧边界对象
console.log("leftPoint", leftPoint);
var boundarys2 = modelHelper.buildAreaBoundary(leftPoint);
boundaryCollection.push(boundarys2);
} else {
let points = [];
//如果交点非相邻(对边)
if (crossObjectArray[0].cross) {
crossObjectArray[0].crossBy.z = crossObjectArray[2].crossBy.z = 0;
points.push(crossObjectArray[3].item[0], crossObjectArray[3].item[1], crossObjectArray[0].crossBy, crossObjectArray[2].crossBy);
boundaryCollection.push(modelHelper.buildAreaBoundary(points));
points = [];
points.push(crossObjectArray[0].crossBy, crossObjectArray[1].item[0], crossObjectArray[1].item[1], crossObjectArray[2].crossBy);
boundaryCollection.push(modelHelper.buildAreaBoundary(points));
} else {
crossObjectArray[1].crossBy.z = crossObjectArray[3].crossBy.z = 0;
points.push(crossObjectArray[0].item[0], crossObjectArray[0].item[1], crossObjectArray[1].crossBy, crossObjectArray[3].crossBy);
boundaryCollection.push(modelHelper.buildAreaBoundary(points));
points = [];
points.push(crossObjectArray[1].crossBy, crossObjectArray[2].item[0], crossObjectArray[2].item[1], crossObjectArray[3].crossBy);
boundaryCollection.push(modelHelper.buildAreaBoundary(points));
}
}
return boundaryCollection;
}
总体效果
目前的空间拆分仅限于矩形空间,因为矩形的空间在BIM
运维中相对来说是比较多的,而且算法相对简单一些,后续我们会逐渐探索非矩形空间,甚至是不规则多边形的空间拆分与合并算法,并应用到空间资产管理与运维场景中。
地址:https://www.cnblogs.com/xhb-bky-blog/p/13500295.html
声明:本博客原创文字只代表本人工作中在某一时间内总结的观点或结论,与本人所在单位没有直接利益关系。非商业,未授权贴子请以现状保留,转载时必须保留此段声明,且在文章页面明显位置给出原文连接。