在我的前一篇博客《C#开发BIMFACE系列42 服务端API之图纸对比》中详细介绍了BIMFACE服务端接口模型对比的功能。 BIMFACE官方文档提供的三维模型对比接口同样也适用于二维CAD图纸对比。下图中是官方提供的对比示例程序。
其中新增的图元使用绿色标记、修改的图元使用黄色标记、删除的图元使用红色标记。
下面介绍BIMFACE图纸对比功能的原理与实现。
图纸对比可以对两个图纸文件进行差异性分析,确定两个图纸文件之间构件的几何和属性差异,包括增加的图元构件、删除的图元和修改的图元。
前置条件特别说明:图纸对比是在BIMFACE云端进行的,通常需要5~10分钟。当模型对比完成后,BIMFACE能通知对比结果。
图纸文件经过云端转换后,生成了BIMFACE定义的数据包。因此,要对比两个图纸文件,实际上需要对比两个文件的数据包。如下图所示,文件B是文件A修改后的版本,对比完成之后,其结果包括两个部分:
BIMFACE提供了服务端API,用于发起对比,获取对比状态、获取对比结果。请参考我的博客:
C#开发BIMFACE系列30 服务端API之模型对比1:发起模型对比 C#开发BIMFACE系列31 服务端API之模型对比2:获取模型对比状态 C#开发BIMFACE系列32 服务端API之模型对比3:批量获取模型对比状态 C#开发BIMFACE系列33 服务端API之模型对比4:获取模型对比结果 C#开发BIMFACE系列34 服务端API之模型对比5:获取模型构建对比差异 测试程序发起图纸对比
调用服务器端的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之图纸拆分》
上述测试程序使用了 《BIMFace.SDK.CSharp》开源SDK。欢迎大家下载使用。
BIMFACE二次开发系列目录 【已更新最新开发文章,点击查看详细】