外部构件(External Object)

BIM模型主要提供的是静态的模型与信息,但在真正的施工、运维场景中,普遍存在需要在既有模型基础上额外添加构件,并对构件进行编辑的需求。这一节我们将在场景中添加一辆汽车,并尝试变动它的位置和方向。

教程内容

在已搭建的运维平台上,接下来我们需要

  • 添加车辆构件
  • 改变车辆的方向
  • 让车辆向前移动

外部构件说明

  • 我们提供了外部构件库,用户可以复制其中的URL来获取资源;
  • 用户也可导入属于自己的构件,当前支持3DS格式的文件导入,后续将开放更多格式;
  • 可添加粒子效果,如火焰效果等;

外部构件应用

1. 添加外部构件

我们先创建一个添加外部构件的按钮:

<button class="button" id="btnAddObject" onclick="addExternalObject()">添加外部构件</button>

接下来,我们要构造一个添加JavaScript脚本的函数,用以在导入3DS对象时使用。

// 加载JavaScript脚本
function loadScript(url, callback) {
  var script = document.createElement("script");
  script.type = "text/javascript";
  script.onload = function () {
    callback && callback();
  }
  script.src = url;
  document.head.appendChild(script);
}

添加脚本后,我们从构件库中选取一个车辆的3DS对象,添加到场景中。

// 添加外部构件
var isExternalObjectAdded = false;
function addExternalObject() {
  if (isExternalObjectAdded) {
    return;
  }
  var objUrl = "http://static.bimface.com/attach/6db9d5bcf88640f997b23be61e870ee8_%E6%B1%BD%E8%BD%A6.3DS";
  // 构造3DS加载器
  var loader = new THREE.TDSLoader();
  // 通过加载器加载资源,获取3DS对象
  loader.load(objUrl, function (object) {
    // 将该对象添加为外部构件
    viewer3D.addExternalObject("vehicle", object);
    isExternalObjectAdded = true;
    viewer3D.render();
  });
}

现在,我们已经将车辆作为外部构件加载到场景中,但目前还看不到构件,原因在于载入的构件默认位置是场景的坐标原点。因此,我们还需要构造一个方法来调整构件的坐标。

// 对外部构件进行平移
function setTransform(name, position) {
  // 获取构件对象
  var object = viewer3D.getExternalObjectByName(name);
  if (!object) {
    return;
  }
  // 构件平移
  if (position) {
    object.position.x += position.x;
    object.position.y += position.y;
    object.position.z += position.z;
  }
  // 更新构件
  object.updateMatrixWorld();
  viewer3D.render();
}

我们将构件移动到小楼外的地坪上,需要在加载构件后添加以下内容。

// 将构件移至初始位置
setTransform("vehicle", new THREE.Vector3(-7500, -15000, -450));

添加外部构件

2. 改变车辆方向

在添加外部构件的过程中,我们已经构造了构件平移的方法,接下来我们要对setTransform进行改造,以便构件可以进行缩放和旋转。

// 对外部构件进行平移、缩放和旋转
function setTransform(name, position, scale, rotation) {
  // 获取构件对象
  var object = viewer3D.getExternalObjectByName(name);
  if (!object) {
    return;
  }
  // 构件平移
  if (position) {
    object.position.x += position.x;
    object.position.y += position.y;
    object.position.z += position.z;
  }
  // 构件缩放
  if (scale) {
    object.scale.x *= scale.x;
    object.scale.y *= scale.y;
    object.scale.z *= scale.z;
  }
  // 构件旋转
  if (rotation) {
    object.rotation.x += rotation.x;
    object.rotation.y += rotation.y;
    object.rotation.z += rotation.z;
  }
  // 更新构件
  object.updateMatrixWorld();
  viewer3D.render();
}

接下来,我们添加一个旋转构件的按钮。

<button class="button" id="btnRotateObject" onclick="rotateObject()">旋转外部构件</button>

通过setTransform函数,我们让构件每次绕逆时针旋转30°。

// 旋转外部构件
function rotateObject() {
  if (!isExternalObjectAdded) {
    return;
  }
  // 绕逆时针旋转30°
  setTransform("vehicle", new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 1, 1), new THREE.Vector3(0, 0, Math.PI / 6));
}

改变车辆方向

3. 移动车辆前进

添加一个平移构件的按钮。

<button class="button" id="btnMovebject" onclick="moveObject()">移动外部构件</button>

最后,我们尝试将车辆每次向前移动1米,考虑到车辆的朝向,我们需要在平移时对距离进行换算。

function moveObject() {
  if (!isExternalObjectAdded) {
    return;
  }
  // 沿汽车前进方向移动1.0米
  var object = viewer3D.getExternalObjectByName("vehicle");
  setTransform("vehicle", new THREE.Vector3(1000 * Math.sin(object.rotation.z), -1000 * Math.cos(object.rotation.z), 0));
}

移动车辆前进

4. 拓展

更多关于构件编辑的应用,可参考以下DEMO内容:

完整代码

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>BIMFACE model scene</title>
    <style type="text/css">
      * {
        margin: 0;
        padding: 0;
      }
      html, body {
        height: 100%;
      }
      .buttons {
        font-size: 0;
      }
      .button {
        margin: 5px 0 5px 5px;
        width: 90px;
        height: 30px;
        border-radius: 3px;
        border: none;
        background: #11DAB7;
        color: #FFFFFF;
      }
      .main {
        display: flex;
        flex-direction: column;
        overflow: hidden;
        height: 100%;
      }
      #domId {
        flex: 1;
      }
    </style>
  </head>
  <body>
    <div class='main'>
      <div class='buttons'>
        <button class="button" id="btnIsolation" onclick="isolateComponents()">构件隔离</button>
        <button class="button" id="btnZoomToSelection" onclick="zoomToSelectedComponents()">构件定位</button>
        <button class="button" id="btnOverrideColor" onclick="overrideComponents()">构件着色</button>
        <button class="button" id="btnBlinkComponent" onclick="blinkComponents()">构件强调</button>
        <button class="button" id="btnSaveState" onclick="getCurrentState()">保存状态</button>
        <button class="button" id="btnRestoreState" onclick="setState()">恢复状态</button>
        <button class="button" id="btnStartAutoRotate" onclick="startAutoRotate()">开始旋转场景</button>
        <button class="button" id="btnAddKeyFrame" onclick="addKeyFrame()">添加关键帧</button>
        <button class="button" id="btnPlayWalkThrough" onclick="playWalkThrough()">播放路径漫游</button>
        <button class="button" id="btnDrawAnnotation" onclick="drawAnnotation()">开始绘制批注</button>
        <button class="button" id="btnRestoreAnnotation" onclick="restoreAnnotation()">恢复批注</button>
        <button class="button" id="btnTagging" onclick="addCustomTag()">开始放置标签</button>
        <button class="button" id="btnAddMarker" onclick="addMarker()">放置三维标签</button>
        <button class="button" id="btnCreateRoom" onclick="createRoom()">创建房间</button>
        <button class="button" id="btnHideRoom" onclick="hideRoom([roomId])">隐藏房间</button>
        <button class="button" id="btnAddObject" onclick="addExternalObject()">添加外部构件</button>
        <button class="button" id="btnRotateObject" onclick="rotateObject()">旋转外部构件</button>
        <button class="button" id="btnMovebject" onclick="moveObject()">移动外部构件</button>

      </div>
      <div id="domId"></div>
    </div>
    <script src="https://static.bimface.com/api/BimfaceSDKLoader/BimfaceSDKLoader@latest-release.js"></script>
    <script>
      var viewToken = '<yourViewToken>';
      // 声明Viewer及App
      var viewer3D;
      var app;
      // 配置JSSDK加载项
      window.onload = function() {
        var loaderConfig = new BimfaceSDKLoaderConfig();
        loaderConfig.viewToken = viewToken;
        BimfaceSDKLoader.load(loaderConfig, successCallback, failureCallback);
      }
      // 加载成功回调函数
      function successCallback(viewMetaData) {
        loadScript("http://static.bimface.com/attach/341bb8bde7bf4a5898ecdf58c2a476fb_TDSLoader.js");
        var dom4Show = document.getElementById('domId');
        // 设置WebApplication3D的配置项
        var webAppConfig = new Glodon.Bimface.Application.WebApplication3DConfig();
        webAppConfig.domElement = dom4Show;
        // 创建WebApplication3D,用以显示模型
        app = new Glodon.Bimface.Application.WebApplication3D(webAppConfig);
        app.addView(viewToken);
        viewer3D = app.getViewer();
      }
      // 加载失败回调函数
      function failureCallback(error) {
        console.log(error);
      }

      // ************************** 隔离 **************************
      var isIsolationActivated = false;
      function isolateComponents() {
        if (!isIsolationActivated) {
          // 设置隔离选项,指定其他构件为半透明状态
          var makeOthersTranslucent = Glodon.Bimface.Viewer.IsolateOption.MakeOthersTranslucent;
          // 调用viewer3D.method,隔离楼层为"F2"的构件
          viewer3D.isolateComponentsByObjectData([{"levelName":"F2"}], makeOthersTranslucent);
          // 渲染三维模型
          viewer3D.render(); 
          // 修改按钮的文字内容
          setButtonText("btnIsolation", "取消隔离");
        } else {
          // 清除隔离
          viewer3D.clearIsolation();
          // 渲染三维模型
          viewer3D.render();
          // 修改按钮的文字内容
          setButtonText("btnIsolation", "构件隔离");
        }
        isIsolationActivated = !isIsolationActivated;
      }

      // ************************** 定位 **************************
      var isZoomToSelectionActivated = false;
      function zoomToSelectedComponents(){
        if (!isZoomToSelectionActivated) {
          // 选中id为"271431"的构件
          viewer3D.addSelectedComponentsById(["271431"]);
          // 定位到选中的构件
          viewer3D.zoomToSelectedComponents();
          // 清除构件选中状态
          viewer3D.clearSelectedComponents();
          setButtonText("btnZoomToSelection", "回到主视角");
        } else {
          // 切换至主视角
          viewer3D.setView(Glodon.Bimface.Viewer.ViewOption.Home);
          setButtonText("btnZoomToSelection", "构件定位");
        }
        isZoomToSelectionActivated = !isZoomToSelectionActivated;
      }

      // ************************** 着色 **************************
      var isOverrideActivated = false;
      function overrideComponents(){
        if (!isOverrideActivated) {
          // 新建color对象,指定关注构件被染色的数值
          var color = new Glodon.Web.Graphics.Color("#11DAB7", 0.5);
          // 对关注构件进行着色
          viewer3D.overrideComponentsColorById(["389601"], color);
          viewer3D.render();
          setButtonText("btnOverrideColor", "清除着色");
        } else {
          // 清除构件着色
          viewer3D.clearOverrideColorComponents();
          viewer3D.render();
          setButtonText("btnOverrideColor", "构件着色");
        }
        isOverrideActivated = !isOverrideActivated;
      }

      // ************************** 构件闪烁 **************************
      var isBlinkActivated = false;
      function blinkComponents() {
        if (!isBlinkActivated) {
          var blinkColor = new Glodon.Web.Graphics.Color("#B22222", 0.8);
          // 打开构件强调开关
          viewer3D.enableBlinkComponents(true);
          // 给需要报警的构件添加强调状态
          viewer3D.addBlinkComponentsById(["389617"]);
          // 设置强调状态下的颜色
          viewer3D.setBlinkColor(blinkColor);
          // 设置强调状态下的频率
          viewer3D.setBlinkIntervalTime(500);
          viewer3D.render();
          setButtonText("btnBlinkComponent", "清除强调");
        } else {
          // 清除构件强调
          viewer3D.clearAllBlinkComponents();
          viewer3D.render();
          setButtonText("btnBlinkComponent", "构件强调");
        }
        isBlinkActivated = !isBlinkActivated;
      }

      // ************************** 状态 **************************
      var state;
      function getCurrentState(){
        // 保存当前模型浏览状态
        state = viewer3D.getCurrentState(); 
      }

      function setState(){
        if (state != null) {
          // 恢复模型浏览状态
          viewer3D.setState(state);
          viewer3D.render();
        } else {
          window.alert("请先保存一个模型浏览状态!");
        }
      }

      // ************************** 旋转场景 **************************
      var isAutoRotateActivated = false;
      function startAutoRotate() {
        if (!isAutoRotateActivated) {
          // 开始场景旋转
          viewer3D.startAutoRotate(5);
          setButtonText("btnStartAutoRotate", "结束旋转场景");
        } else {
          // 结束场景旋转
          viewer3D.stopAutoRotate();
          setButtonText("btnStartAutoRotate", "开始旋转场景");
        }
        isAutoRotateActivated = !isAutoRotateActivated;
      }

      // ************************** 路径漫游 **************************
      var walkThrough = null;
      function createWalkThrough() {
        if (walkThrough == null) {
          // 构造路径漫游配置wtConfig
          var walkThroughConfig = new Glodon.Bimface.Plugins.Walkthrough.WalkthroughConfig();
          // 设置路径漫游配置匹配的viewer对象
          walkThroughConfig.viewer = viewer3D;
          // 构造路径漫游对象
          walkThrough = new Glodon.Bimface.Plugins.Walkthrough.Walkthrough(walkThroughConfig);
        }
      }

      function addKeyFrame() {
        createWalkThrough();
        //添加关键帧
        walkThrough.addKeyFrame();
      }

      function playWalkThrough() {
        if (walkThrough != null) {
          // 设置播放时间为5秒
          walkThrough.setWalkthroughTime(5);
          // 设置关键帧事件
          walkThrough.setKeyFrameCallback(kfCallback);
          // 播放路径漫游
          walkThrough.play();
        } else {
          window.alert("Please add keyframes first.");
        }
      }

      function kfCallback(idx) {
        switch (idx) {
          case 0:
            break;
          case 1:
            console.log('Hello, BIM!');
            break;
        }
      }

      // ************************** 批注 **************************
      var isDrawAnnotationActivated = false;
      var annotationToolbar = null;
      var annotationState = null;
      function createAnnotationToolbar() {
        if (!annotationToolbar) {
          // 创建批注工具条的配置
          var config = new Glodon.Bimface.Plugins.Annotation.AnnotationToolbarConfig();
          config.viewer = viewer3D;
          // 创建批注工具条
          annotationToolbar = new Glodon.Bimface.Plugins.Annotation.AnnotationToolbar(config);
          // 注册批注工具条的监听事件
          annotationToolbar.addEventListener(Glodon.Bimface.Plugins.Annotation.AnnotationToolbarEvent.Saved, onAnnotationSaved);
          annotationToolbar.addEventListener(Glodon.Bimface.Plugins.Annotation.AnnotationToolbarEvent.Cancelled, eixtAnnotation);
        }
      }

      // 保存批注并退出
      function onAnnotationSaved() {
        annotationState = annotationToolbar.getAnnotationManager().getCurrentState();
        eixtAnnotation();
      }

      // 退出批注
      function eixtAnnotation() {
        // 显示主工具条
        app.getToolbar("MainToolbar").show();
        annotationToolbar.getAnnotationManager().exit();
        // 批注的激活状态为false
        isDrawAnnotationActivated = false;
      }

      function drawAnnotation() {
        // 创建批注工具条
        createAnnotationToolbar();
        if (!isDrawAnnotationActivated) {
          // 隐藏主工具条
          app.getToolbar("MainToolbar").hide();
          // 显示批注工具条
          annotationToolbar.show();
          // 修改批注的激活状态为true
          isDrawAnnotationActivated = true;
        } 
      }

      function restoreAnnotation() {
        if (annotationState != null) {
          // 恢复批注
          annotationToolbar.getAnnotationManager().setState(annotationState);
        } else {
          window.alert("Please draw an annotation first.");
        }
      }

      // ************************** 自定义标签 **************************
      var isAddCustomTagActivated = false;
      var customItemContainer = null;
      function createCustomItemContainer() {
        if (!customItemContainer) {
          // 创建标签容器配置
          var drawableContainerConfig = new Glodon.Bimface.Plugins.Drawable.DrawableContainerConfig();
          // 设置容器配置匹配的对象
          drawableContainerConfig.viewer = viewer3D;
          // 创建标签容器
          customItemContainer = new Glodon.Bimface.Plugins.Drawable.DrawableContainer(drawableContainerConfig);
        }
      }

      function addCustomItem(object) {
        createCustomItemContainer();
        // 创建CustomItemConfig
        var config = new Glodon.Bimface.Plugins.Drawable.CustomItemConfig();
        var content = document.createElement('div');
        // 自定义样式,支持HTML的任意DOM元素
        // 设置标签的宽度和高度
        content.style.width = '80px';
        content.style.height = '32px';
        // 设置标签样式
        content.style.border = 'solid';
        content.style.borderColor = '#FFFFFF';
        content.style.borderWidth = '2px';
        content.style.borderRadius = '5%';
        content.style.background = '#11DAB7';
        // 设置标签文字内容与样式
        content.innerText = '检查点';
        content.style.color = '#FFFFFF';
        content.style.textAlign = 'center';
        content.style.lineHeight = '32px';
        // 设置自定义标签配置
        config.content = content;
        config.viewer = viewer3D;
        config.worldPosition = object.worldPosition;
        // 创建自定义标签对象
        var customItem = new Glodon.Bimface.Plugins.Drawable.CustomItem(config);
        // 将自定义标签添加至标签容器内
        customItemContainer.addItem(customItem)
      }

      function addCustomTag() {
        if (!isAddCustomTagActivated) {
          // 创建鼠标点击的监听事件
          viewer3D.addEventListener(Glodon.Bimface.Viewer.Viewer3DEvent.MouseClicked, addCustomItem);
          setButtonText("btnTagging", "结束放置标签");
        } else {
          // 移除鼠标点击的监听事件
          viewer3D.removeEventListener(Glodon.Bimface.Viewer.Viewer3DEvent.MouseClicked, addCustomItem);
          setButtonText("btnTagging", "开始放置标签");
        }
        isAddCustomTagActivated = !isAddCustomTagActivated;
      }

      // ************************** 内建三维标签 **************************
      var is3DMarkerPlaced = false;
      function addMarker() {
        if (is3DMarkerPlaced) {
          return;
        }
        // 构造三维标签容器配置markerContainerConfig
        var markerContainerConfig = new Glodon.Bimface.Plugins.Marker3D.Marker3DContainerConfig();
        // 设置markerContainerConfig匹配的viewer对象
        markerContainerConfig.viewer = viewer3D;
        // 构造三维标签容器markerContainer
        var markerContainer = new Glodon.Bimface.Plugins.Marker3D.Marker3DContainer(markerContainerConfig);
        // 构造三维标签配置项
        var markerConfig = new Glodon.Bimface.Plugins.Marker3D.Marker3DConfig();
        // 为标签指定图片路径
        markerConfig.src = "http://static.bimface.com/resources/3DMarker/warner/warner_red.png";
        // 构造点位,并指定为标签的插入点
        var markerPos = {"x": -5743.838548165086, "y": -1667.12605781937, "z": 12923.137945446013};
        markerConfig.worldPosition = markerPos;
        // 指定标签大小
        markerConfig.size = 60;
        // 构造三维标签
        var marker = new Glodon.Bimface.Plugins.Marker3D.Marker3D(markerConfig);
        // 添加标签的点击事件
        marker.onClick(function() {
          window.alert('Warning!');
        });
        // 将三维标签添加至容器内
        markerContainer.addItem(marker);
        is3DMarkerPlaced = true;
      }

      // ************************** 房间创建与应用 **************************
      var isRoomCreated = false;
      var isHideRoomActivated = false;
      var roomId;
      function createRoom() {
        if (isRoomCreated) {
          return;
        }
        // 获取解析后的房间边界信息,这里要找到名称为“办公室 11”的对象,并创建房间
        viewer3D.getAreas(function(data) {
          if (data) {
            for (var i = 0; i < data.length; i++) {
              if (data[i].rooms) {
                for (var j = 0; j < data[i].rooms.length; j++) {
                  if (data[i].rooms[j].name == "办公室 11") {
                    // 获取房间信息和id
                    var roomInfo = data[i].rooms[j];
                    roomId = 'room-' + roomInfo.id;
                    // 通过房间边界创建一个3350mm高的房间,并自定义房间id
                    viewer3D.addArea(roomInfo.boundary, 3350, roomId);
                    // 将房间颜色设置为红色
                    setRoomRed([roomId]);
                    viewer3D.render();
                    isRoomCreated = true;
                  }
                }
              }
            }
          }
        });
      }

      // 设置房间颜色为红色
      function setRoomRed(ids) {
        // 设置房间线框颜色
        var redColor1 = new Glodon.Web.Graphics.Color(255, 0, 0, 0.8);
        viewer3D.setAreasFrameColorById(ids, redColor1);
        // 设置房间颜色
        var redColor2 = new Glodon.Web.Graphics.Color(255, 0, 0, 0.1);
        viewer3D.setAreasColorById(ids, redColor2);
      }

      function hideRoom(ids) {
        // 判断是否已经创建房间
        if (!isRoomCreated) {
          window.alert("Please create a room first!");
          return;
        }
        if (!isHideRoomActivated) {
          // 根据id隐藏房间
          viewer3D.hideAreasById(ids);
          viewer3D.render();
          setButtonText("btnHideRoom", "显示房间");
          isHideRoomActivated = true;
        } else {
          // 根据id显示房间
          viewer3D.showAreasById(ids);
          viewer3D.render();
          setButtonText("btnHideRoom", "隐藏房间");
          isHideRoomActivated = false;
        }
      }

      // ************************** 添加外部构件 **************************
      // 添加外部构件
      var isExternalObjectAdded = false;
      function addExternalObject() {
        if (isExternalObjectAdded) {
          return;
        }
        var objUrl = "http://static.bimface.com/attach/6db9d5bcf88640f997b23be61e870ee8_%E6%B1%BD%E8%BD%A6.3DS";
        // 构造3DS加载器
        var loader = new THREE.TDSLoader();
        // 通过加载器加载资源,获取3DS对象
        loader.load(objUrl, function (object) {
          // 将该对象添加为外部构件
          viewer3D.addExternalObject("vehicle", object);
          isExternalObjectAdded = true;
          // 将构件移至初始位置
          setTransform("vehicle", new THREE.Vector3(-7500, -15000, -450));
          viewer3D.render();
        });
      }

      // 对外部构件进行平移、缩放和旋转
      function setTransform(name, position, scale, rotation) {
        // 获取构件对象
        var object = viewer3D.getExternalObjectByName(name);
        if (!object) {
          return;
        }
        // 构件平移
        if (position) {
          object.position.x += position.x;
          object.position.y += position.y;
          object.position.z += position.z;
        }
        // 构件缩放
        if (scale) {
          object.scale.x *= scale.x;
          object.scale.y *= scale.y;
          object.scale.z *= scale.z;
        }
        // 构件旋转
        if (rotation) {
          object.rotation.x += rotation.x;
          object.rotation.y += rotation.y;
          object.rotation.z += rotation.z;
        }
        // 更新构件
        object.updateMatrixWorld();
        viewer3D.render();
      }

      // 旋转外部构件
      function rotateObject() {
        if (!isExternalObjectAdded) {
          return;
        }
        // 绕逆时针旋转30°
        setTransform("vehicle", new THREE.Vector3(0, 0, 0), new THREE.Vector3(1, 1, 1), new THREE.Vector3(0, 0, Math.PI / 6));
      }

      // 移动外部构件
      function moveObject() {
        if (!isExternalObjectAdded) {
          return;
        }
        // 沿汽车前进方向移动1.0米
        var object = viewer3D.getExternalObjectByName("vehicle");
        setTransform("vehicle", new THREE.Vector3(1000 * Math.sin(object.rotation.z), -1000 * Math.cos(object.rotation.z), 0));
      }

      // 加载js脚本
      function loadScript(url, callback) {
        var script = document.createElement("script");
        script.type = "text/javascript";
        script.onload = function () {
          callback && callback();
        }
        script.src = url;
        document.head.appendChild(script);
      }

      // ************************** 按钮文字 **************************
      function setButtonText(btnId, text) {
        var dom = document.getElementById(btnId);
        if (dom != null && dom.nodeName == "BUTTON") {
          dom.innerText = text;
        }
      }
    </script>
  </body>
</html>

至此,你已经完成了全部进阶模式的学习,如需要运用BIMFACE的其他功能,可查看API的介绍文档,或是查看示例DEMO来完成具体功能的实现。