在建築施工圖審查系統中,設計單位提交設計完成的模型/圖紙,審查專家審查模型/圖紙。審查過程中如果發現不符合規範的地方,則流程退回到設計單位,設計單位人員根據審查意見重新調整設計,調整完成後再次提交到審查專家。此時為了便於專家審查,需要知道當前輪次的模型/圖紙與上一輪次的模型/圖紙發生了哪些異動,針對異動情況進行審查即可。
先看效果
效果如上圖。左側是當前審查輪次的模型,中間是上一輪次的模型,右側是2個模型的對比產生的異動列表。
(1)點選“新增構建”中的構件,自動定位到當前輪次中新增的目標構件。異動構件以淺綠色表示。2個模型視角同步移動。
(2)點選“刪除構建”中的構件,自動定位到上一輪次中的目標構件,本輪次中的構件被刪除,所以不顯示。異動構件以淺綠色表示。2個模型視角同步移動。
(3)點選“修改構建”中的構件,自動定位到當前輪次中修改的構件以及上一輪次對應的構件。異動構件以淺綠色表示。2個模型視角同步移動。
BIMFACE之前是沒有三維模型聯動對比的功能,在我和BIMFACE的技術支援團隊的美麗小姐姐溝通後,他們把我的要求納入了他們產品的需求,經過工程師們加班加點的辛苦付出,很快就實現了該功能。特此感謝BIMFACE團隊的所有小夥伴,感謝你們對開發者的信任與接受,感謝你們的辛苦付出。
滴水之恩,當湧泉相報,奉獻上BIMFace C#版SDK開源專案。
開源地址:https://gitee.com/NAlps/BIMFace.SDK
作者:張傳寧(系統架構師、技術總監 南京群耀 http://www.sparkcn.com) QQ:905442693 微信:savionzhang
歡迎下載使用,交流、分享。
下面介紹BIMFACE模型對比功能的原理與實現。
模型對比可以對兩個檔案/模型進行差異性分析,確定兩個檔案/模型之間構件的幾何和屬性差異,包括增加的構件、刪除的構件和修改的構件。 模型對應可以用於進行檔案/模型的版本對比。
特別說明:模型對比是在BIMFACE雲端進行的,通常需要5~10分鐘。當模型對比完成後,BIMFACE能通知對比結果。
- 您需要將修改前和修改後的模型上傳到雲端並轉換成功以後才能發起模型對比;
- 目前僅支援.rvt單檔案的模型對比。
- 通過服務端API發起模型對比(對比前後模型檔案的fileId);
- 等待雲端對比任務執行;
- 對比完成後,在網頁端通過呼叫JavaScript API實現差異模型的顯示;
- 除了顯示差異模型,還需要呼叫服務端API獲取對比結果(包括新增、刪除、修改的構件列表)。
模型檔案經過雲端轉換後,生成了BIMFACE定義的資料包。因此,要對比兩個模型檔案,實際上需要對比兩個檔案的資料包。如下圖所示,檔案B是檔案A修改後的版本,對比完成之後,其結果包括兩個部分:
- 幾何差異;
- 變更構件及屬性。
BIMFACE提供了服務端API,用於發起對比,獲取對比狀態、獲取對比結果。請參考我的部落格:
發起模型對比
呼叫伺服器端的API獲取對比結果
對比差異分為三類:新增、修改、刪除。返回結果對應的實體類如下:
1 /// <summary> 2 /// 模型對比差異類 3 /// </summary> 4 public class ModelCompareDiff 5 { 6 /// <summary> 7 /// 對比差異構件所屬類別ID。樣例 : "-2001320" 8 /// </summary> 9 [JsonProperty("categoryId", NullValueHandling = NullValueHandling.Ignore)] 10 public string CategoryId { get; set; } 11 12 /// <summary> 13 /// 對比差異構件所屬類別名稱。樣例 : "framework" 14 /// </summary> 15 [JsonProperty("categoryName", NullValueHandling = NullValueHandling.Ignore)] 16 public string CategoryName { get; set; } 17 18 /// <summary> 19 /// 對比構件差異型別:NEW、DELETE、CHANGE 20 /// </summary> 21 [JsonProperty("diffType", NullValueHandling = NullValueHandling.Ignore)] 22 public string DiffType { get; set; } 23 24 /// <summary> 25 /// 對比差異構件ID。樣例 : "296524" 26 /// </summary> 27 [JsonProperty("elementId", NullValueHandling = NullValueHandling.Ignore)] 28 public string ElementId { get; set; } 29 30 /// <summary> 31 /// 對比差異構件名稱 32 /// </summary> 33 [JsonProperty("elementName", NullValueHandling = NullValueHandling.Ignore)] 34 public string ElementName { get; set; } 35 36 /// <summary> 37 /// 對比差異構件的族名稱。樣例 : "framework 1" 38 /// </summary> 39 [JsonProperty("family", NullValueHandling = NullValueHandling.Ignore)] 40 public string Family { get; set; } 41 42 /// <summary> 43 /// 對比差異構件來源構件ID。樣例 : "0213154515478" 44 /// </summary> 45 [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] 46 public string Id { get; set; } 47 48 /// <summary> 49 /// 對比差異構件變更檔案ID,即(當前)變更後的檔案ID。樣例 : "1136893002033344" 50 /// </summary> 51 [JsonProperty("followingFileId", NullValueHandling = NullValueHandling.Ignore)] 52 public string FollowingFileId { get; set; } 53 54 /// <summary> 55 /// 對比差異構件來原始檔ID,即 (歷史)變更前的檔案ID。樣例 : "0213154515478" 56 /// </summary> 57 [JsonProperty("previousFileId", NullValueHandling = NullValueHandling.Ignore)] 58 public string PreviousFileId { get; set; } 59 60 /// <summary> 61 /// 對比差異構件所屬專業。樣例 : "civil" 62 /// </summary> 63 [JsonProperty("specialty", NullValueHandling = NullValueHandling.Ignore)] 64 public string Specialty { get; set; } 65 }
返回的差異結果如下:
1 { 2 "code": "success", 3 "message": null, 4 "data": { 5 "data": [{ 6 "diffType": "NEW", 7 "id": "1946876" 8 }, { 9 "diffType": "NEW", 10 "id": "1946877" 11 }, { 12 "diffType": "NEW", 13 "id": "1946878" 14 }, { 15 "diffType": "CHANGE", 16 "id": "40539" 17 }, { 18 "diffType": "CHANGE", 19 "id": "40541" 20 }, { 21 "diffType": "CHANGE", 22 "id": "40542" 23 }, { 24 "diffType": "DELETE", 25 "id": "22243" 26 } 27 ], 28 "page": 1, 29 "total": 7 30 } 31 }
前端使用JS來實現同步聯動效果,以及點選異動構件後自動定位到構件所在的視角。
1 // 同步新舊模型的平移和旋轉操作 2 function correspond() { 3 latestViewer = latest.getViewer(); 4 var state, focus; 5 6 prevViewer = prev.getViewer(); 7 view1Bind = function (data) { 8 //用新模型的狀態更新舊模型狀態 9 var latestState = latestViewer.getCurrentState(); 10 prev.setState(latestState); 11 prev.getViewer().camera.up.copy(latestViewer.getViewer().camera.up); 12 } 13 14 view2Bind = function (data) { 15 //用舊模型的狀態更新新模型狀態 16 var prevState = prev.getCurrentState(); 17 latestViewer.setState(prevState); 18 latestViewer.getViewer().camera.up.copy(prev.getViewer().camera.up); 19 } 20 21 //考慮到死迴圈的影響,不能同時監聽render事件,因此以滑鼠所在位置模型的監聽為主 22 document.querySelector('#container').addEventListener('mousemove', 23 function (e) { 24 if (focus == undefined) { 25 var width = document.querySelector('.latest').offsetWidth; 26 if (e.clientX > width) { 27 focus = 1; 28 latestViewer.removeEventListener('Rendered', view1Bind); 29 prev.addEventListener('Rendered', view2Bind); 30 } else { 31 focus = 0; 32 prev.removeEventListener('Rendered', view2Bind); 33 latestViewer.addEventListener('Rendered', view1Bind); 34 } 35 } 36 }); 37 38 view1.addEventListener('mouseover', 39 function (e) { 40 if (focus == 0) { 41 return; 42 } 43 focus = 0; 44 // 解綁與重新繫結事件,同步新舊模型的Rendered 45 prev.removeEventListener('Rendered', view2Bind); 46 latestViewer.addEventListener('Rendered', view1Bind); 47 }); 48 49 view2.addEventListener('mouseover', 50 function () { 51 if (focus == 1) { 52 return; 53 } 54 focus = 1; 55 // 解綁與重新繫結事件,同步新舊模型的Rendered 56 latestViewer.removeEventListener('Rendered', view1Bind); 57 prev.addEventListener('Rendered', view2Bind); 58 }); 59 60 // 同步新舊模型的Hover事件 61 prev.addEventListener('ComponentsHoverChanged', 62 function (e) { 63 latestViewer.getViewer().modelManager.sceneState.setHoverId(e.objectId); 64 }); 65 66 latestViewer.addEventListener('ComponentsHoverChanged', 67 function (e) { 68 prev.getViewer().modelManager.sceneState.setHoverId(e.objectId); 69 }); 70 71 var ViewerEvent = Glodon.Bimface.Viewer.Viewer3D; 72 latestViewer.setCameraAnimation(false); 73 prev.setCameraAnimation(false); 74 }
1 //建立異動列表與構件的click事件 2 function createDom(result) { 3 // 設定構件差異構件樹的UI 4 var newItems = result.newItems, 5 deleteItems = result.deleteItems, 6 changeItems = result.changeItems; 7 var typeBoxs = document.querySelectorAll('.type-box'); 8 typeBoxs[0].innerHTML = 9 `<div class="title"><i class="icon arrow"></i><i class="icon-type new"></i>新增構件(${newItems.length})</div> 10 <ul id="addElement" class="type-ul">${createDomNode(newItems)}</ul>`; 11 12 // 刪除構件列表 13 typeBoxs[1].innerHTML = 14 `<div class="title"><i class="icon arrow"></i><i class="icon-type remove"></i>刪除構件(${deleteItems.length})</div> 15 <ul id="removeElement" class="type-ul">${createDomNode(deleteItems)}</ul>`; 16 17 // 修改構件列表 18 typeBoxs[2].innerHTML = 19 `<div class="title"><i class="icon arrow"></i><i class="icon-type revise"></i>修改構件(${changeItems.length})</div> 20 <ul id="reviseElement" class="type-ul">${createDomNode(changeItems)}</ul>`; 21 22 // 差異構件樹列表 23 document.querySelector('.compare-content').addEventListener('click', 24 function (e) { 25 var element = e.target; 26 if (element.tagName == 'I' && element.hasClass('arrow')) { 27 if (element.hasClass('close')) { 28 element.removeClass('close'); 29 element.parentElement.nextElementSibling.removeClass('close'); 30 } else { 31 element.addClass('close'); 32 element.parentElement.nextElementSibling.addClass('close'); 33 } 34 } else if (element.tagName == 'SPAN' && element.getAttribute('type')) { 35 var type = element.getAttribute('type'), 36 id = element.parentElement.getAttribute('data-oid'); 37 if (type == 'NEW') { 38 latestViewer.setSelectedComponentsById([id]);// 高亮選中構件 39 latestViewer.zoomToSelectedComponents(); // 定位 40 view1Bind(); 41 } else if (type == 'DELETE') { 42 prev.setSelectedComponentsById([id]);// 高亮選中構件 43 prev.zoomToSelectedComponents(); // 定位 44 view2Bind(); 45 } else { 46 latestViewer.setSelectedComponentsById([id]); // 高亮選中構件 47 latestViewer.zoomToSelectedComponents();// 定位; 48 view1Bind(); 49 prev.setSelectedComponentsById([id]); 50 } 51 } 52 }); 53 }
上述測試程式使用了 《BIMFace.SDK.CSharp》開源SDK。歡迎大家下載使用。