在我的前一篇博客《C#开发BIMFACE系列42 服务端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之图纸拆分》
上述测试程序使用了 《BIMFace.SDK.CSharp》开源SDK。欢迎大家下载使用。