在建筑施工图审查系统中,设计单位提交设计完成的模型/图纸,审查专家审查模型/图纸。审查过程中如果发现不符合规范的地方,则流程退回到设计单位,设计单位人员根据审查意见重新调整设计,调整完成后再次提交到审查专家。此时为了便于专家审查,需要知道当前轮次的模型/图纸与上一轮次的模型/图纸发生了哪些异动,针对异动情况进行审查即可。
先看效果
效果如上图。左侧是当前审查轮次的模型,中间是上一轮次的模型,右侧是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 ///2 /// 模型对比差异类 3 /// 4 public class ModelCompareDiff 5 { 6 /// 7 /// 对比差异构件所属类别ID。样例 : "-2001320" 8 /// 9 [JsonProperty("categoryId", NullValueHandling = NullValueHandling.Ignore)] 10 public string CategoryId { get; set; } 11 12 /// 13 /// 对比差异构件所属类别名称。样例 : "framework" 14 /// 15 [JsonProperty("categoryName", NullValueHandling = NullValueHandling.Ignore)] 16 public string CategoryName { get; set; } 17 18 /// 19 /// 对比构件差异类型:NEW、DELETE、CHANGE 20 /// 21 [JsonProperty("diffType", NullValueHandling = NullValueHandling.Ignore)] 22 public string DiffType { get; set; } 23 24 /// 25 /// 对比差异构件ID。样例 : "296524" 26 /// 27 [JsonProperty("elementId", NullValueHandling = NullValueHandling.Ignore)] 28 public string ElementId { get; set; } 29 30 /// 31 /// 对比差异构件名称 32 /// 33 [JsonProperty("elementName", NullValueHandling = NullValueHandling.Ignore)] 34 public string ElementName { get; set; } 35 36 /// 37 /// 对比差异构件的族名称。样例 : "framework 1" 38 /// 39 [JsonProperty("family", NullValueHandling = NullValueHandling.Ignore)] 40 public string Family { get; set; } 41 42 /// 43 /// 对比差异构件来源构件ID。样例 : "0213154515478" 44 /// 45 [JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)] 46 public string Id { get; set; } 47 48 /// 49 /// 对比差异构件变更文件ID,即(当前)变更后的文件ID。样例 : "1136893002033344" 50 /// 51 [JsonProperty("followingFileId", NullValueHandling = NullValueHandling.Ignore)] 52 public string FollowingFileId { get; set; } 53 54 /// 55 /// 对比差异构件来源文件ID,即 (历史)变更前的文件ID。样例 : "0213154515478" 56 /// 57 [JsonProperty("previousFileId", NullValueHandling = NullValueHandling.Ignore)] 58 public string PreviousFileId { get; set; } 59 60 /// 61 /// 对比差异构件所属专业。样例 : "civil" 62 /// 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。欢迎大家下载使用。