【BIM】基於BIMFACE的空間拆分與合並


BIMFACE中矩形空間拆分與合並

應用場景

BIM運維場景中,空間同設備一樣,作為一種資產被納入運維管理體系,典型的應用場景例如商鋪、防火分區等,這就涉及到空間的拆分和合並,在bimface中,已經實現了空間的動態調整,但是距離自定義的,較為直觀的空間拆分與合並,目前的處理方式還不能夠滿足業務場景的需求,於是自行完成了基於bimface的矩形空間的拆分與合並的實現過程。

空間拆分與合並 拆分空間 監聽模型單擊事件 1 清除原有空間 點擊計數 根據兩點繪制直線 2 任意兩點決定直線走向 計算直線在坐標系中的斜率A和截距b 計算直線與各邊界交點 3 通過直線方程計算與各邊的交點 剔除不符合條件的交點 判別拆分類型 4 切點鄰邊 計算公共點 計算交點鄰接點 點集排序 切點對邊 區分交點與原始點集 點分組排序 切點對角線 直接按對角線進行點排序 計算拆分后的邊界對象 5 根據排序后的點集生成可識別的邊界對象 繪制空間 6 合並空間 邊界數據整理 1 驗證數據合法性(可選) 2 結構轉換存儲 3 篩選極值 4 構造邊界對象 5 繪制空間 6

先說合並

合並矩形空間的前提條件,是有兩個及以上的且相鄰的矩形空間,如果兩個空間不相鄰,也就失去了合並的意義,即使合並也不能夠表達出真實物理世界的空間結構。空間合並相對來說比較簡單,每個空間都是有一系列的有序的點圍起來的二維封閉平面,這一系列的點集暫且稱之為邊界信息點集,加上高度參數就形成了三維立體空間。相鄰的矩形空間必然會有近似重合的點,如下圖的黃圈部分,如果把這些點去掉,只保留最外圍的點(極值點,如下圖的白圈部分),就形成了一個新的有序的點集,構成了新的邊界信息,再加上合理的高度,被合並的空間就產生了。

空間合並示意圖

以下是空間合並的核心代碼:

/**
 * 空間合並處理管道,適用於多個規則且相鄰的矩形空間
 * @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
聲明:本博客原創文字只代表本人工作中在某一時間內總結的觀點或結論,與本人所在單位沒有直接利益關系。非商業,未授權貼子請以現狀保留,轉載時必須保留此段聲明,且在文章頁面明顯位置給出原文連接。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM