在我的前一篇部落格《C#開發BIMFACE系列41 服務端API之模型對比》中詳細介紹了BIMFACE服務端介面模型對比的功能。 BIMFACE官方文件提供的三維模型對比介面同樣也適用於二維CAD圖紙對比。下圖中是官方提供的對比示例程式。
其中新增的圖元使用綠色標記、修改的圖元使用黃色標記、刪除的圖元使用紅色標記。
下面介紹BIMFACE圖紙對比功能的原理與實現。
圖紙對比可以對兩個圖紙檔案進行差異性分析,確定兩個圖紙檔案之間構件的幾何和屬性差異,包括增加的圖元構件、刪除的圖元和修改的圖元。
特別說明:圖紙對比是在BIMFACE雲端進行的,通常需要5~10分鐘。當模型對比完成後,BIMFACE能通知對比結果。
- 您需要將修改前和修改後的圖紙上傳到雲端並轉換成功以後才能發起圖紙對比;
- 目前支援.dwg、.dwf單檔案的圖紙對比。
- 通過服務端API發起圖紙對比(對比前後模型檔案的fileId);
- 等待雲端對比任務執行;
- 對比完成後,在網頁端通過呼叫JavaScript API實現差異圖紙的顯示;
- 除了顯示差異圖紙,還需要呼叫服務端API獲取對比結果(包括新增、刪除、修改的圖元列表)。
圖紙檔案經過雲端轉換後,生成了BIMFACE定義的資料包。因此,要對比兩個圖紙檔案,實際上需要對比兩個檔案的資料包。如下圖所示,檔案B是檔案A修改後的版本,對比完成之後,其結果包括兩個部分:
- 幾何差異;
- 變更構件及屬性。
BIMFACE提供了服務端API,用於發起對比,獲取對比狀態、獲取對比結果。請參考我的部落格:
發起圖紙對比
呼叫伺服器端的API獲取對比結果
對比差異分為三類:新增、修改、刪除。由於CAD圖紙的展示型別包含 Model 與 Layer 兩種形式,
差異結果中也是包含兩種展示型別的對比資訊,所以可能有重複的圖元ID,需要手動過濾。
返回結果對應的實體類如下
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 "layer": "D1", 9 "sheetId": "0", 10 "sheetName": "Model", 11 "type": "Model" 12 }, { 13 "diffType": "NEW", 14 "id": "1946877", 15 "layer": "D1", 16 "sheetId": "0", 17 "sheetName": "Model", 18 "type": "Model" 19 }, { 20 "diffType": "NEW", 21 "id": "1946878", 22 "layer": "D1", 23 "sheetId": "0", 24 "sheetName": "Model", 25 "type": "Model" 26 }, { 27 "diffType": "CHANGE", 28 "id": "40539", 29 "layer": "0", 30 "sheetId": "0", 31 "sheetName": "Model", 32 "type": "Model" 33 }, { 34 "diffType": "CHANGE", 35 "id": "40541", 36 "layer": "0", 37 "sheetId": "0", 38 "sheetName": "Model", 39 "type": "Model" 40 }, { 41 "diffType": "CHANGE", 42 "id": "40542", 43 "layer": "0", 44 "sheetId": "0", 45 "sheetName": "Model", 46 "type": "Model" 47 }, { 48 "diffType": "CHANGE", 49 "id": "22243", 50 "layer": "AXIS", 51 "sheetId": "0", 52 "sheetName": "Model", 53 "type": "Model" 54 } 55 ], 56 "page": 1, 57 "total": 7 58 } 59 }
網頁中使用JS來實現圖紙展示與差異對比效果,以及點選異動圖元后自動定位到構件所在的視角。官網示例請參考 https://bimface.com/developer-jsdemo#988
官網的對比展示效果是將2張圖紙進行疊加對比顯示的,下面介紹另一種對比展示方式,2張圖紙分別展示,左側展示當前版本圖紙,右側展示歷史版本圖紙。
點選新增圖元項,自動定位(綠色標記)
點選修改圖元項,自動定位(黃色標記)
點選刪除圖元項,自動定位(紅色標記)
佈局如下
<body> <div class="nav"><a class="lg"><b>xxxx圖紙.dwg</b></a></div> <div id="container"> <div class='latest'> <!--<div class='title'> <span>當前輪次(<b>當前版本</b>)</span> </div>--> </div> <div class='prev'> <!--<div class='title'> <span>上一輪次(<b>歷史版本</b>)</span> </div>--> </div> <div class="list"> <h3>差異列表(<span>0</span>)</h3> <div class="detail"> <ul class="bf-collapse add"> <span class="bf-icon"></span> <span>新增圖元(<b>0</b>)</span> <div class="items"></div> </ul> <ul class="bf-collapse edit"> <span class="bf-icon"></span> <span>修改圖元(<b>0</b>)</span> <div class="items"></div> </ul> <ul class="bf-collapse deletes"> <span class="bf-icon"></span> <span>刪除圖元(<b>0</b>)</span> <div class="items"></div> </ul> </div> </div> </div> </body>
指令碼實現圖紙載入展示
1 $(document).ready(function () { 2 document.querySelector('.nav .lg b').innerHTML = sclc_desc + "【" + tzFileName1 + "】" + " 對比 【" + tzFileName2 + "】"; 3 4 var success = getViewTokens(compareId); 5 if (!success) { 6 return; 7 } 8 9 prev = previousFileViewToken; 10 latest = followingFileViewToken; 11 compare = compareViewToken; 12 13 var bimfaceLoaderConfig = new BimfaceSDKLoaderConfig(); 14 bimfaceLoaderConfig.viewToken = latest; 15 BimfaceSDKLoader.load(bimfaceLoaderConfig, onSDKLoadSucceeded, onSDKLoadFailed); 16 }); 17 18 function onSDKLoadSucceeded(viewMetaData) { 19 if (viewMetaData.viewType == "drawingView") { 20 // 載入修改後圖紙 21 var webAppConfig = new Glodon.Bimface.Application.WebApplicationDrawingConfig(); 22 webAppConfig.domElement = document.querySelector('.latest'); 23 latest = new Glodon.Bimface.Application.WebApplicationDrawing(webAppConfig); 24 latest.load(viewMetaData.viewToken); 25 26 // 載入修改前圖紙 27 latest.getViewer().getViewMetaData(prev, 28 function (viewMetaData) { 29 var webAppConfig = new Glodon.Bimface.Application.WebApplicationDrawingConfig(); 30 webAppConfig.domElement = document.querySelector('.prev'); 31 prev = new Glodon.Bimface.Viewer.ViewerDrawing(webAppConfig); 32 prev.load(viewMetaData.viewToken); 33 prev.addEventListener('Loaded', correspond); 34 }); 35 36 $.ajax({ 37 url: "Handlers/GetBIMCompareResultFromDBHandler.ashx", 38 data: { compareId: compareId, modelType: '2D' }, 39 dataType: "json", 40 type: "GET", 41 async: false, //同步。函式有返回值,必修設定為同步執行 42 success: function (data) { 43 if (data.code == true) { 44 var add = '', edit = '', deletes = ''; 45 if (data.news) { 46 data.news.map((item, i) => { 47 add += `<li class='add-item'>${item.elementId}</li>`; 48 }); 49 document.querySelector('.add .items').innerHTML = add; 50 document.querySelector('.add b').innerHTML = data.news.length; 51 } 52 if (data.changes) { 53 data.changes.map((item, i) => { 54 edit += `<li class='modify-item'>${item.elementId}</li>`; 55 }); 56 document.querySelector('.edit .items').innerHTML = edit; 57 document.querySelector('.edit b').innerHTML = data.changes.length; 58 } 59 if (data.deletes) { 60 data.deletes.map((item, i) => { 61 deletes += `<li class='delete-item'>${item.elementId}</li>`; 62 }); 63 document.querySelector('.deletes .items').innerHTML = deletes; 64 document.querySelector('.deletes b').innerHTML = data.deletes.length; 65 } 66 document.querySelector('.list h3 span').innerHTML = 67 (data.deletes ? data.deletes.length * 1 : 0) + 68 (data.changes ? data.changes.length * 1 : 0) + 69 (data.news ? data.news.length * 1 : 0); 70 } else { 71 $.messager.alert('提示', data.message, 'warning'); 72 } 73 }, 74 error: function (e) { 75 $.messager.alert('提示', e, 'error'); 76 } 77 }); 78 } else { 79 $.messager.alert('提示', '對比的檔案不是二維圖紙。', 'warning'); 80 } 81 }; 82 83 function onSDKLoadFailed(error) { 84 alert("圖紙載入失敗。"); 85 };
指令碼實現差異項點選事件
1 // 同步新舊圖紙的平移和旋轉操作 2 function correspond() { 3 prevViewer = prev.getViewer(); 4 latestViewer = latest.getViewer(); 5 var state; 6 bindEvent(); 7 (latestViewer.getViewer()).onViewChanges = function () { 8 if (latestViewer.getCurrentState() == state) { 9 return; 10 } 11 state = latestViewer.getCurrentState(); 12 prev.setState(state); 13 } 14 15 setTimeout(function () { 16 prevViewer.onViewChanges = function () { 17 if (prev.getCurrentState() == state) { 18 return; 19 } 20 state = prev.getCurrentState(); 21 latestViewer.getViewer().setState(state); 22 } 23 }, 24 10); 25 26 // 同步新舊圖紙的HOVER事件和CLICK事件 27 //let ViewerEvent = Glodon.Bimface.Viewer.ViewerDrawingEvent; 28 var ViewerEvent = Glodon.Bimface.Viewer.ViewerDrawingEvent; 29 30 latestViewer.addEventListener(ViewerEvent.ComponentsSelectionChanged, 31 function (data) { 32 prev.clearSelection(); 33 prev.selectByIds(data); 34 }); 35 36 prev.addEventListener(ViewerEvent.ComponentsSelectionChanged, 37 function (data) { 38 latestViewer.getViewer().clearSelection(); 39 latestViewer.selectByIds(data); 40 }); 41 42 latestViewer.addEventListener(ViewerEvent.Hover, 43 function (data) { 44 prev.clearHighlight(); 45 data.objectId && prev.highlightById(data.objectId); 46 console.log(data.objectId); 47 }); 48 49 prev.addEventListener(ViewerEvent.Hover, 50 function (data) { 51 latestViewer.getViewer().clearHighlight(); 52 data.objectId && latestViewer.getViewer().highlightById(data.objectId); 53 }); 54 } 55 56 function bindEvent() { 57 var red = new Glodon.Web.Graphics.Color("#FF0000", 0.8); 58 var yellow = new Glodon.Web.Graphics.Color("#FFF68F", 0.8); 59 var blue = new Glodon.Web.Graphics.Color("#32CD99", 0.8); 60 // 設定差異列表的互動 61 // 獲取文件中 class="detail" 的第一個元素: 差異列表內容的div 62 var dom = document.querySelector('.detail'); 63 64 // 差異列表的點選事件 65 // e 為MouseEvent事件,其target為點選到的html元素 66 dom.addEventListener('click', 67 function (e) { 68 console.log(e); 69 var target = e.target; 70 tagName = target.tagName; 71 // 通過點選物件的種類,決定互動 72 if (tagName == 'SPAN') { 73 // 如果是span,則展開/收起列表 74 target.parentElement.toggleClass('bf-collapse'); 75 } else if (tagName == 'LI') { 76 // 如果是li,則繪製矩形框 77 // 獲取點選的數值,對應圖元的id 78 var id = target.innerText; 79 80 // 清除上一步的選中效果和boundingBox 81 latest.getViewer().clearSelection(); 82 latest.getViewer().clearElementBox(); 83 prev.clearElementBox(); 84 prev.clearSelection(); 85 86 switch (target.className) { 87 // 新增圖元 88 case "add-item": 89 // 設定矩形框的樣式-藍色&雲線 90 prev.setElementBoxColor(blue); 91 prev.setElementBoxStyle("CloudRect"); 92 latest.getViewer().setElementBoxColor(blue); 93 latest.getViewer().setElementBoxStyle("CloudRect"); 94 95 // 定位 96 latest.getViewer().zoomToObject(id); 97 98 // 繪製矩形框 99 var BBox = latest.getViewer().getObjectBoundingBox(parseInt(id)); 100 prev.showElementBoxByBBox(BBox, 1); 101 console.log(BBox); 102 latest.getViewer().showElementBoxByBBox(BBox, 1); 103 break; 104 105 // 被修改圖元 106 case "modify-item": 107 // 設定矩形框的樣式-黃色&雲線 108 prev.setElementBoxColor(yellow); 109 prev.setElementBoxStyle("CloudRect"); 110 latest.getViewer().setElementBoxColor(yellow); 111 latest.getViewer().setElementBoxStyle("CloudRect"); 112 113 // 定位 114 prev.zoomToObject(id); 115 116 // 繪製矩形框 117 var BBox = prev.getViewer().getObjectBoundingBox(parseInt(id)); 118 prev.showElementBoxByBBox(BBox, 1); 119 latest.getViewer().showElementBoxByBBox(BBox, 1); 120 break; 121 122 // 被刪除圖元 123 case "delete-item": 124 // 設定矩形框的樣式-紅色&雲線 125 prev.setElementBoxColor(red); 126 prev.setElementBoxStyle("CloudRect"); 127 latest.getViewer().setElementBoxColor(red); 128 latest.getViewer().setElementBoxStyle("CloudRect"); 129 130 // 定位 131 prev.zoomToObject(id); 132 133 // 繪製矩形框 134 var BBox = prev.getViewer().getObjectBoundingBox(parseInt(id)); 135 prev.showElementBoxByBBox(BBox, 1); 136 latest.getViewer().showElementBoxByBBox(BBox, 1); 137 } 138 } 139 }); 140 141 // 設定layout切換同步 142 var layout = document.querySelector('.bf-family .bf-sub-toolbar'); 143 layout.addEventListener('click', 144 function (e) { 145 var target = e.target, tagName = target.tagName, name, views; 146 if (tagName == 'SPAN') { 147 name = target.innerText; 148 } else if (tagName == 'DIV') { 149 name = target.getAttribute('title'); 150 } 151 views = prev.getViews(); 152 views.map((item, i) => { 153 if (item.name == name) { 154 prev.showViewById(item.id); 155 } 156 }); 157 }); 158 159 // 顯示效果同步 160 var state = { showLineWidth: true, mode: '普通模式', layout: 'model' } 161 setInterval(() => { 162 var lineWidth = latest.getViewer().getViewer().viewer.ShowLineWidth; 163 var container = document.querySelectorAll('.bf-drawing-container '); 164 if (lineWidth != state.showLineWidth) { 165 state.showLineWidth = !state.showLineWidth; 166 prev.showLineWidth(state.showLineWidth); 167 } 168 if (document.querySelector('input[mode=普通模式]') && 169 document.querySelector('input[mode=普通模式]').checked && 170 (state.mode != '普通模式')) { 171 state.mode = '普通模式'; 172 173 prev.setPrintMode('Normal'); 174 container[1].style.background = 'rgb(50,50,55)'; 175 176 } else if (document.querySelector('input[mode=白底模式]') && 177 document.querySelector('input[mode=白底模式]').checked && 178 (state.mode != '白底模式')) { 179 state.mode = '白底模式'; 180 prev.setPrintMode('White'); 181 container[1].style.background = 'rgb(255,255,255)'; 182 } else if (document.querySelector('input[mode=黑白模式]') && 183 document.querySelector('input[mode=黑白模式]').checked && 184 (state.mode != '黑白模式')) { 185 state.mode = '黑白模式'; 186 prev.setPrintMode('Black'); 187 container[1].style.background = 'rgb(255,255,255)'; 188 } 189 }, 190 1000); 191 192 // 圖層列表顯示同步 193 var watch = function () { 194 var layers = document.querySelector('.layers-panel'); 195 if (layers) { 196 layers.addEventListener('click', 197 function (e) { 198 var data = latest.getViewer().getViewer().getLayers(), 199 obj = {}, 200 arr = [], 201 prevState = prev.getLayers(); 202 data.map(function (item, index) { 203 obj[item.id] = item; 204 }); 205 prevState.map(function (item, index) { 206 if (obj[item.id]) { 207 arr.push(obj[item.id]); 208 } else { 209 arr.push(item); 210 } 211 }); 212 prev.getViewer().changeLayers(arr); 213 prev.getViewer().update(); 214 }); 215 } else { 216 setTimeout(watch, 1000); 217 } 218 } 219 watch(); 220 }
官方提供的示例中,對比的2個.dwg檔案中,每個檔案中僅包含一張圖紙,即一個圖框。在常規業務場景下,一個.dwg檔案中包含多個圖框,如下圖
當前版本與歷史版本對比完成後,通過上述測試程式,在Web網頁中點選差異項可以自動定位到圖元變化所在位置。是否可以知道差異項來自哪個圖框呢?
答案是肯定的,實現方案參考下面兩篇部落格《C#開發BIMFACE系列43 服務端API之圖紙拆分》、《C#開發BIMFACE系列44 服務端API之計算圖紙對比差異項來源自哪個圖框》。
上述測試程式使用了 《BIMFace.SDK.CSharp》開源SDK。歡迎大家下載使用。