
在我的前一篇博客《C#开发BIMFACE系列41 服务端API之模型对比》中详细介绍了BIMFACE服务端接口模型对比的功能。 BIMFACE官方文档提供的三维模型对比接口同样也适用于二维CAD图纸对比。下图中是官方提供的对比示例程序。

其中新增的图元使用绿色标记、修改的图元使用黄色标记、删除的图元使用红色标记。
下面介绍BIMFACE图纸对比功能的原理与实现。
图纸对比可以对两个图纸文件进行差异性分析,确定两个图纸文件之间构件的几何和属性差异,包括增加的图元构件、删除的图元和修改的图元。
特别说明:图纸对比是在BIMFACE云端进行的,通常需要5~10分钟。当模型对比完成后,BIMFACE能通知对比结果。
前置条件
- 您需要将修改前和修改后的图纸上传到云端并转换成功以后才能发起图纸对比;
- 目前支持.dwg、.dwf单文件的图纸对比。
基本步骤
- 通过服务端API发起图纸对比(对比前后模型文件的fileId);
- 等待云端对比任务执行;
- 对比完成后,在网页端通过调用JavaScript API实现差异图纸的显示;
- 除了显示差异图纸,还需要调用服务端API获取对比结果(包括新增、删除、修改的图元列表)。
对比流程
图纸文件经过云端转换后,生成了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,需要手动过滤。
返回结果对应的实体类如下
/// <summary>
/// 模型对比差异类
/// </summary>
public class ModelCompareDiff
{
/// <summary>
/// 对比差异构件所属类别ID。样例 : "-2001320"
/// </summary>
[JsonProperty("categoryId", NullValueHandling = NullValueHandling.Ignore)]
public string CategoryId { get; set; }
/// <summary>
/// 对比差异构件所属类别名称。样例 : "framework"
/// </summary>
[JsonProperty("categoryName", NullValueHandling = NullValueHandling.Ignore)]
public string CategoryName { get; set; }
/// <summary>
/// 对比构件差异类型:NEW、DELETE、CHANGE
/// </summary>
[JsonProperty("diffType", NullValueHandling = NullValueHandling.Ignore)]
public string DiffType { get; set; }
/// <summary>
/// 对比差异构件ID。样例 : "296524"
/// </summary>
[JsonProperty("elementId", NullValueHandling = NullValueHandling.Ignore)]
public string ElementId { get; set; }
/// <summary>
/// 对比差异构件名称
/// </summary>
[JsonProperty("elementName", NullValueHandling = NullValueHandling.Ignore)]
public string ElementName { get; set; }
/// <summary>
/// 对比差异构件的族名称。样例 : "framework 1"
/// </summary>
[JsonProperty("family", NullValueHandling = NullValueHandling.Ignore)]
public string Family { get; set; }
/// <summary>
/// 对比差异构件来源构件ID。样例 : "0213154515478"
/// </summary>
[JsonProperty("id", NullValueHandling = NullValueHandling.Ignore)]
public string Id { get; set; }
/// <summary>
/// 对比差异构件变更文件ID,即(当前)变更后的文件ID。样例 : "1136893002033344"
/// </summary>
[JsonProperty("followingFileId", NullValueHandling = NullValueHandling.Ignore)]
public string FollowingFileId { get; set; }
/// <summary>
/// 对比差异构件来源文件ID,即 (历史)变更前的文件ID。样例 : "0213154515478"
/// </summary>
[JsonProperty("previousFileId", NullValueHandling = NullValueHandling.Ignore)]
public string PreviousFileId { get; set; }
/// <summary>
/// 对比差异构件所属专业。样例 : "civil"
/// </summary>
[JsonProperty("specialty", NullValueHandling = NullValueHandling.Ignore)]
public string Specialty { get; set; }
}
对比结果如下
{
"code": "success",
"message": null,
"data": {
"data": [{
"diffType": "NEW",
"id": "1946876",
"layer": "D1",
"sheetId": "0",
"sheetName": "Model",
"type": "Model"
}, {
"diffType": "NEW",
"id": "1946877",
"layer": "D1",
"sheetId": "0",
"sheetName": "Model",
"type": "Model"
}, {
"diffType": "NEW",
"id": "1946878",
"layer": "D1",
"sheetId": "0",
"sheetName": "Model",
"type": "Model"
}, {
"diffType": "CHANGE",
"id": "40539",
"layer": "0",
"sheetId": "0",
"sheetName": "Model",
"type": "Model"
}, {
"diffType": "CHANGE",
"id": "40541",
"layer": "0",
"sheetId": "0",
"sheetName": "Model",
"type": "Model"
}, {
"diffType": "CHANGE",
"id": "40542",
"layer": "0",
"sheetId": "0",
"sheetName": "Model",
"type": "Model"
}, {
"diffType": "CHANGE",
"id": "22243",
"layer": "AXIS",
"sheetId": "0",
"sheetName": "Model",
"type": "Model"
}
],
"page": 1,
"total": 7
}
}
网页中使用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>
脚本实现图纸加载展示
$(document).ready(function () {
document.querySelector('.nav .lg b').innerHTML = sclc_desc + "【" + tzFileName1 + "】" + " 对比 【" + tzFileName2 + "】";
var success = getViewTokens(compareId);
if (!success) {
return;
}
prev = previousFileViewToken;
latest = followingFileViewToken;
compare = compareViewToken;
var bimfaceLoaderConfig = new BimfaceSDKLoaderConfig();
bimfaceLoaderConfig.viewToken = latest;
BimfaceSDKLoader.load(bimfaceLoaderConfig, onSDKLoadSucceeded, onSDKLoadFailed);
});
function onSDKLoadSucceeded(viewMetaData) {
if (viewMetaData.viewType == "drawingView") {
// 加载修改后图纸
var webAppConfig = new Glodon.Bimface.Application.WebApplicationDrawingConfig();
webAppConfig.domElement = document.querySelector('.latest');
latest = new Glodon.Bimface.Application.WebApplicationDrawing(webAppConfig);
latest.load(viewMetaData.viewToken);
// 加载修改前图纸
latest.getViewer().getViewMetaData(prev,
function (viewMetaData) {
var webAppConfig = new Glodon.Bimface.Application.WebApplicationDrawingConfig();
webAppConfig.domElement = document.querySelector('.prev');
prev = new Glodon.Bimface.Viewer.ViewerDrawing(webAppConfig);
prev.load(viewMetaData.viewToken);
prev.addEventListener('Loaded', correspond);
});
$.ajax({
url: "Handlers/GetBIMCompareResultFromDBHandler.ashx",
data: { compareId: compareId, modelType: '2D' },
dataType: "json",
type: "GET",
async: false, //同步。函数有返回值,必修设置为同步执行
success: function (data) {
if (data.code == true) {
var add = '', edit = '', deletes = '';
if (data.news) {
data.news.map((item, i) => {
add += `<li class='add-item'>${item.elementId}</li>`;
});
document.querySelector('.add .items').innerHTML = add;
document.querySelector('.add b').innerHTML = data.news.length;
}
if (data.changes) {
data.changes.map((item, i) => {
edit += `<li class='modify-item'>${item.elementId}</li>`;
});
document.querySelector('.edit .items').innerHTML = edit;
document.querySelector('.edit b').innerHTML = data.changes.length;
}
if (data.deletes) {
data.deletes.map((item, i) => {
deletes += `<li class='delete-item'>${item.elementId}</li>`;
});
document.querySelector('.deletes .items').innerHTML = deletes;
document.querySelector('.deletes b').innerHTML = data.deletes.length;
}
document.querySelector('.list h3 span').innerHTML =
(data.deletes ? data.deletes.length * 1 : 0) +
(data.changes ? data.changes.length * 1 : 0) +
(data.news ? data.news.length * 1 : 0);
} else {
$.messager.alert('提示', data.message, 'warning');
}
},
error: function (e) {
$.messager.alert('提示', e, 'error');
}
});
} else {
$.messager.alert('提示', '对比的文件不是二维图纸。', 'warning');
}
};
function onSDKLoadFailed(error) {
alert("图纸加载失败。");
};
脚本实现差异项点击事件
// 同步新旧图纸的平移和旋转操作
function correspond() {
prevViewer = prev.getViewer();
latestViewer = latest.getViewer();
var state;
bindEvent();
(latestViewer.getViewer()).onViewChanges = function () {
if (latestViewer.getCurrentState() == state) {
return;
}
state = latestViewer.getCurrentState();
prev.setState(state);
}
setTimeout(function () {
prevViewer.onViewChanges = function () {
if (prev.getCurrentState() == state) {
return;
}
state = prev.getCurrentState();
latestViewer.getViewer().setState(state);
}
},
10);
// 同步新旧图纸的HOVER事件和CLICK事件
//let ViewerEvent = Glodon.Bimface.Viewer.ViewerDrawingEvent;
var ViewerEvent = Glodon.Bimface.Viewer.ViewerDrawingEvent;
latestViewer.addEventListener(ViewerEvent.ComponentsSelectionChanged,
function (data) {
prev.clearSelection();
prev.selectByIds(data);
});
prev.addEventListener(ViewerEvent.ComponentsSelectionChanged,
function (data) {
latestViewer.getViewer().clearSelection();
latestViewer.selectByIds(data);
});
latestViewer.addEventListener(ViewerEvent.Hover,
function (data) {
prev.clearHighlight();
data.objectId && prev.highlightById(data.objectId);
console.log(data.objectId);
});
prev.addEventListener(ViewerEvent.Hover,
function (data) {
latestViewer.getViewer().clearHighlight();
data.objectId && latestViewer.getViewer().highlightById(data.objectId);
});
}
function bindEvent() {
var red = new Glodon.Web.Graphics.Color("#FF0000", 0.8);
var yellow = new Glodon.Web.Graphics.Color("#FFF68F", 0.8);
var blue = new Glodon.Web.Graphics.Color("#32CD99", 0.8);
// 设置差异列表的交互
// 获取文档中 class="detail" 的第一个元素: 差异列表内容的div
var dom = document.querySelector('.detail');
// 差异列表的点击事件
// e 为MouseEvent事件,其target为点击到的html元素
dom.addEventListener('click',
function (e) {
console.log(e);
var target = e.target;
tagName = target.tagName;
// 通过点击对象的种类,决定交互
if (tagName == 'SPAN') {
// 如果是span,则展开/收起列表
target.parentElement.toggleClass('bf-collapse');
} else if (tagName == 'LI') {
// 如果是li,则绘制矩形框
// 获取点击的数值,对应图元的id
var id = target.innerText;
// 清除上一步的选中效果和boundingBox
latest.getViewer().clearSelection();
latest.getViewer().clearElementBox();
prev.clearElementBox();
prev.clearSelection();
switch (target.className) {
// 新增图元
case "add-item":
// 设置矩形框的样式-蓝色&云线
prev.setElementBoxColor(blue);
prev.setElementBoxStyle("CloudRect");
latest.getViewer().setElementBoxColor(blue);
latest.getViewer().setElementBoxStyle("CloudRect");
// 定位
latest.getViewer().zoomToObject(id);
// 绘制矩形框
var BBox = latest.getViewer().getObjectBoundingBox(parseInt(id));
prev.showElementBoxByBBox(BBox, 1);
console.log(BBox);
latest.getViewer().showElementBoxByBBox(BBox, 1);
break;
// 被修改图元
case "modify-item":
// 设置矩形框的样式-黄色&云线
prev.setElementBoxColor(yellow);
prev.setElementBoxStyle("CloudRect");
latest.getViewer().setElementBoxColor(yellow);
latest.getViewer().setElementBoxStyle("CloudRect");
// 定位
prev.zoomToObject(id);
// 绘制矩形框
var BBox = prev.getViewer().getObjectBoundingBox(parseInt(id));
prev.showElementBoxByBBox(BBox, 1);
latest.getViewer().showElementBoxByBBox(BBox, 1);
break;
// 被删除图元
case "delete-item":
// 设置矩形框的样式-红色&云线
prev.setElementBoxColor(red);
prev.setElementBoxStyle("CloudRect");
latest.getViewer().setElementBoxColor(red);
latest.getViewer().setElementBoxStyle("CloudRect");
// 定位
prev.zoomToObject(id);
// 绘制矩形框
var BBox = prev.getViewer().getObjectBoundingBox(parseInt(id));
prev.showElementBoxByBBox(BBox, 1);
latest.getViewer().showElementBoxByBBox(BBox, 1);
}
}
});
// 设置layout切换同步
var layout = document.querySelector('.bf-family .bf-sub-toolbar');
layout.addEventListener('click',
function (e) {
var target = e.target, tagName = target.tagName, name, views;
if (tagName == 'SPAN') {
name = target.innerText;
} else if (tagName == 'DIV') {
name = target.getAttribute('title');
}
views = prev.getViews();
views.map((item, i) => {
if (item.name == name) {
prev.showViewById(item.id);
}
});
});
// 显示效果同步
var state = { showLineWidth: true, mode: '普通模式', layout: 'model' }
setInterval(() => {
var lineWidth = latest.getViewer().getViewer().viewer.ShowLineWidth;
var container = document.querySelectorAll('.bf-drawing-container ');
if (lineWidth != state.showLineWidth) {
state.showLineWidth = !state.showLineWidth;
prev.showLineWidth(state.showLineWidth);
}
if (document.querySelector('input[mode=普通模式]') &&
document.querySelector('input[mode=普通模式]').checked &&
(state.mode != '普通模式')) {
state.mode = '普通模式';
prev.setPrintMode('Normal');
container[1].style.background = 'rgb(50,50,55)';
} else if (document.querySelector('input[mode=白底模式]') &&
document.querySelector('input[mode=白底模式]').checked &&
(state.mode != '白底模式')) {
state.mode = '白底模式';
prev.setPrintMode('White');
container[1].style.background = 'rgb(255,255,255)';
} else if (document.querySelector('input[mode=黑白模式]') &&
document.querySelector('input[mode=黑白模式]').checked &&
(state.mode != '黑白模式')) {
state.mode = '黑白模式';
prev.setPrintMode('Black');
container[1].style.background = 'rgb(255,255,255)';
}
},
1000);
// 图层列表显示同步
var watch = function () {
var layers = document.querySelector('.layers-panel');
if (layers) {
layers.addEventListener('click',
function (e) {
var data = latest.getViewer().getViewer().getLayers(),
obj = {},
arr = [],
prevState = prev.getLayers();
data.map(function (item, index) {
obj[item.id] = item;
});
prevState.map(function (item, index) {
if (obj[item.id]) {
arr.push(obj[item.id]);
} else {
arr.push(item);
}
});
prev.getViewer().changeLayers(arr);
prev.getViewer().update();
});
} else {
setTimeout(watch, 1000);
}
}
watch();
}
问题思考 ???
官方提供的示例中,对比的2个.dwg文件中,每个文件中仅包含一张图纸,即一个图框。在常规业务场景下,一个.dwg文件中包含多个图框,如下图

当前版本与历史版本对比完成后,通过上述测试程序,在Web网页中点击差异项可以自动定位到图元变化所在位置。是否可以知道差异项来自哪个图框呢?
答案是肯定的,实现方案参考下面两篇博客《C#开发BIMFACE系列43 服务端API之图纸拆分》、《C#开发BIMFACE系列44 服务端API之计算图纸对比差异项来源自哪个图框》。
#新娘出嫁 弟弟挂霸气横幅#
#江苏政法委原书记王立科被逮捕#
#美国跟塔利班又谈崩了?#
#台风“圆规”已加强为强热带风暴#
#犯罪团伙头目“少爷”竟是富二代#