stp2browser3.0 ——为解析超大型CAD文件而生

此版本相对于2.0版本更新了程序运行的系统架构。docker容器的基本系统采用了全新的构建方式。其底层的FreeCAD和opencascade代码都是因本公司的情况,对源代码做了修改并进行了重新编译。可以说,从3.0版本开始,我们可以完全控制支撑我们业务的主要底层代码。

stp2browser3.0版本是为了解析超大型CAD文件而生的,450M以上的step文件能够正常完成解析。此版本在功能上加强了step文件异常数据处理的能力,它能解析普通的step解析器无法解析的step文件,并能够对各种编码具备智能识别能力,完美解决多国语言导致step文件名称出现乱码的问题。

由于此版本主要的底层c++代码都是经过了针对性定制化的编译,其在内存占用量上也得到了很大的优化。

stp2browser交互文档总览

本文提供给与stp2browser项目做交互的技术人员阅读用的技术文档。与stp2browser项目交互包括两个方面。其一是与stp2browser服务器通信,其二是在前端调用haoyu-tuo-plugin插件应用3D预览功能。

stp2browser服务器提供api接口实现预渲染step文件,haoyu-tuo-plugin插件实现前端的3D预览。关于stp2browser服务器的交互文档请参考这里。关于haoyu-tuo-plugin的插件的使用文档,请参考这里。haoyu-tuo-plugin插件文档是加密文档,需要阅读请直接联系本公司人员。

stp2browser2.0服务器接口文档

简介

stp2browser2.0提供云端step文件的预渲染功能,将step文件中拓扑几何结构和材质信息解析成能直接导入webgl读取的2进制文件。此解析将能保留step文件装配体中的逻辑结构,对面和线做分别处理。

目前,stp2browser2.0服务器的应用方式主要是搭配其他位于同一服务器中的应用共同使用。stp2browser2.0应用与其所搭配的应用需要使用同一个文件系统(或者通过虚拟机共享文件夹的形式)。

HTTP请求

stp2browser2.0接受2个http请求,一个是转化申请请求(POST /step/convert body: FormData),一个是状态查询请求(GET step/status/<uid>)。转化申请请求将step文件发送给stp2browser2.0服务器,状态查询请求用于查询指定文件转化的状态。以下,将对这两个请求做详细说明。

转化申请请求

转化申请请求会向服务器发送一个step文件,请求转化运算。请求发送http示例代码如下:

  function processFile(file, uid) 
  {


    let uid =  uid;
    let formData = new FormData();

    formData.append("file", file);
    formData.append("uid", uid);  
  
    fetch('/step/convert/', {method: "POST", body: formData}).then(result => result.json())
      .then(json => {
        if(json.result === 'ok'){
          //上传文件成功,3s后可以开始查询此任务的状态
        }else if(json.result === 'error'){
          if(json.msg === 'unknown'){ 
            throw('unknown error')
          }
          else if(json.msg === 'wrong file format')
          {
            //上传文件uid失败,错误的step文件格式
            throw('wrong step file format')
          }
          else{

            //try again after 10 seconds
            //服务器忙碌,10秒后重新尝试
            ...
          }
        }
      })
      .catch(err => {
        //未知错误处理
        console.log('we have problem...:', err);
      })
}

如示例代码所示,转化申请请求是一个post类型的http请求,它的body中有个formData结构。formData结构中包含两条信息,file和uid。file是通过浏览器上传的File格式数据,uid是一个字符串类型数据,指的是本次转化请求的唯一标识符。如果本次请求转化与之前请求过的任务的唯一标志符重复,则本次请求所产生的信息将会完全覆盖上一次请求所产生的信息。

转化申请请求返回的数据有两大类的情况。第一,请求成功。返回如下json数据:

{
result: 'ok',
}

第二,请求失败。有如下几种可能的情况:
1.文件格式错误
{
"result": "error",
"msg": "wrong file format"
}
推荐处理方式:取消本次请求,检查step文件。
2. http请求格式错误
{
"result": "error",
"msg": "wrong request"
}
推荐处理方式:取消本次请求,检查http请求格式是否符合本文上一段落的描述
3. 服务器忙碌
{
"result": "error",
"msg": "busy"
}
推荐处理方式:稍等10秒钟之后再次尝试转化请求
注意:stp2browser服务器最多只能同时处理10个转化任务,当转化任务达到10个时,再发送转化请求,则会返回此错误。
4. 指定uid的任务正在转化运算中(正在转化运算中的任务不能覆盖)
{
"result": "error",
"msg": "converting"
}
推荐处理方式:稍等10秒钟之后再次尝试转化请求
5. 未知的错误
{
"result": "error",
"msg": "unknown"
}
推荐处理方式:取消本次请求

状态查询请求

状态查询请求负责向服务器查询uid指定任务的转化情况。每当转化请求成功后,服务器就会创建一个转化运算任务。此转化运算任务有如下几种状态:

  1. error, 转化任务运算发生错误
  2. pending, 转化任务正在排队
  3. converting, 转化任务正在进行运算
  4. finished, 转化任务已经完成

状态查询请求示例代码如下:

  function getGltfUntilSuccess(uid, t){

    fetch(`/step/status/${uid}`).then(result => result.json())
      .then(json => {
        if(json.status === 'pending'){
          //set message pending
          //文件${uid}的处理任务正在服务器排队..
          console.log('pending...')
          //等待10s之后再次请求
          ...
        }else if(json.status === 'converting'){
          let pendingTime = json.pendingTime
          //set message converting
          console.log('converting...')
          console.log('pendingTime:', pendingTime, ' seconds')
          //文件${uid}的处理任务正在运算!排队耗时 ${pendingTime} 秒
          //等待3s之后再次请求
          ...
        }else if(json.status === 'finished'){
          let pendingTime = json.pendingTime
          let convertTime = json.convertTime
          console.log('finished')
          console.log('pendingTime:', pendingTime, ' seconds')
          console.log('convertTime:', convertTime, ' seconds')
          //文件${uid}的处理任务已经完成!排队耗时 ${pendingTime} 秒,运算耗时 ${convertTime} 秒 
          //向服务器直接请求预渲染之后的文件
          //HaoyuTuoPlugin.setObject('gltf/' + uid + '/' + uid, null, uid)

        }else{
          //error
          console.log('convert failed, please check your step file');
          //文件${uid}的处理任务失败,请检查step文件格式是否正确
        }
      }).catch((e) => {
        console.log('unknown problems');
        //文件${uid}的处理任务失败,未知的错误!
      })
  }

状态查询请求为GET类型的http请求,请求url为/step/status/<uid>。这里的uid与转化请求中的uid是同一个唯一标识符的概念。这个请求的返回值有以下4种情况:

  1. 任务正在排队
    {
    "status": "pending",
    "result": "ok"
    }
    推荐处理方式: 10s后再次请求
  2. 任务正在运算
    {
    "status": "converting",
    "result": "ok",
    "pendingTime": pendingTime_In_Seconds
    }
    推荐处理方式:3s后再次请求
  3. 任务完成
    {
    "status": "finished",
    "result": "ok",
    "pendingTime": pendingTime_In_Seconds,
    "convertTime": convertTime_In_Seconds
    }
    推荐处理方式:直接向服务器发送读取预渲染文件的请求(关于预渲染文件在服务器上的存在形式请参照下一个段落)
  4. 任务错误
    {
    "status": "error",
    "result": "ok",
    "pendingTime": pendingTime_In_Seconds,
    "convertTime": convertTime_In_Seconds
    }
    推荐处理方式: 取消本次请求,提示转化错误信息,检查并重新上传step文件

预渲染文件的存储

目前的stp2browser2.0服务器采用的是直接将预渲染文件存储在服务器本地文件系统的方案。预渲染文件的位置可以依据部署环境而自定义。自定义的方式也依具体部署方式方法而定。由于配置方法涉及到更多的服务器相关技术应用,本文暂不对如何配置预渲染文件位置的方法做描述,具体的位置配置由北京拓扑拓科技有限公司负责完成。

与stp2browser2.0服务器搭配的应用可以直接以访问静态资源的形式或许预渲染文件。

整体代码示例

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
    <meta name="author" content="北京拓扑拓科技有限公司">
    <meta name="keywords" content="3D建模 移动APP 外包 WebAPP 智能建模APP">
    <meta name="theme-color" content="#000000" />
    <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: blob: 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' http://* ata: content: data: blob:;">
    <link rel="shortcut icon" href="favicon.ico">
    <link rel="stylesheet" type="text/css" href="w3.css">
    <title>Step Browser Simple Demo Production</title>
    <style>
			body {
				font-family: Monospace;
				background-color: #f0f0f0;
				margin: 0px;
				overflow: hidden;
			}
      #root{
        width: 640px;
        height: 640px;
        position: relative 
      }
      @media only screen and (max-width: 600px) {
        #root {
          width: 100vw;
          height: 60vh;
        }
      }
      #msg{
        position: absolute;
        left: 0px;
        bottom:100%;
        width: 100%;
        max-height: 320px;
        padding: 16px;
        overflow-y: auto;
        background-color: rgba(120,120,120,0.3);
        box-shadow: 1px 1px 2px 2px rgba(0,0,0,0.1);
        display: none
      }
    </style>
    <script src="js/three.min.js"></script>
    <script src="tuo-three.6013da4e.js"></script>
    <script src="haoyu-tuo-plugin.b062e342.js"></script>
  </head>
  <body>
    <div class="w3-contanier">
      <input class="w3-input" onchange="onFileChange(event)" type='file'/>
      <button class="w3-button w3-white w3-margin" onclick="onUpload(event)">Upload </button>
    </div>
    <div id="root"></div>
    <div class="w3-contanier" style="position:relative">
      <button class="w3-button w3-white w3-margin" id="detail" 
        onclick="switchMsgStat(event)">show detail</button>
      <div
        id = "msg"
      >
      </div>
    </div>
    <script>
      HaoyuTuoPlugin.init("root")
      var msg = ''
      var isMsgOpen = false
      function updateMsg(){
        msgNode = document.getElementById('msg')
        msgNode.innerText = msg
        msgNode.scrollTop = msgNode.scrollHeight;

        if(isMsgOpen === false)
          showMsg()
      }

      function showMsg(){
        msgNode = document.getElementById('msg')
        msgNode.style.display = "block"
        msgButton = document.getElementById('detail')
        msgButton.innerText = 'hide detail'
        isMsgOpen = true
      }

      function hideMsg(){
        msgNode = document.getElementById('msg')
        msgNode.style.display = "none"
        msgButton = document.getElementById('detail')
        msgButton.innerText = 'show detail'
        isMsgOpen = false
      }

      function switchMsgStat(){
        if(isMsgOpen)
          hideMsg()
        else
          showMsg()
      }

      function appendMsg(text){
        let dtext = '\n' + new Date() + ': ' + text
        msg += dtext
        updateMsg()
      }


      let stepFile
      function onFileChange(event){
        if(event.target.files.length > 0){
          stepFile = event.target.files[0]
        }
        else
          stepFile = null
      }

      function onUpload(event){
        if(stepFile){
          uploadFile(stepFile) 
          //fetch('/convert/upload', {method: "GET"})
        }
      }

      function getDelayPromise(delay){
        return new Promise((resolve, reject) => {
          setTimeout(resolve, delay || 1000)
        })
      }


      var curTime

      function getGltfUntilSuccess(uid, t){
        //to prevent repeat
        if(t !== curTime)
          return
        fetch(`/step/status/${uid}`).then(result => result.json())
          .then(json => {
            if(json.status === 'pending'){
              //set message pending
              appendMsg(`文件${uid}的处理任务正在服务器排队..`)    
              console.log('pending...')
              return getDelayPromise(10000).then(() => getGltfUntilSuccess(uid, t))
            }else if(json.status === 'converting'){
              let pendingTime = json.pendingTime
              //set message converting
              console.log('converting...')
              console.log('pendingTime:', pendingTime, ' seconds')
              appendMsg(`文件${uid}的处理任务正在运算!排队耗时 ${pendingTime} 秒`)    
              return getDelayPromise(3000).then(() => getGltfUntilSuccess(uid, t))
            }else if(json.status === 'finished'){
              let pendingTime = json.pendingTime
              let convertTime = json.convertTime
              console.log('finished')
              console.log('pendingTime:', pendingTime, ' seconds')
              console.log('convertTime:', convertTime, ' seconds')
              appendMsg(`文件${uid}的处理任务正在运算!排队耗时 ${pendingTime} 秒`)    
              if(t !== curTime)
                return
              appendMsg(`文件${uid}的处理任务已经完成!排队耗时 ${pendingTime} 秒,运算耗时 ${convertTime} 秒`)    
              HaoyuTuoPlugin.setObject('gltf/' + uid + '/' + uid, null, uid)
              
            }else{
              //error
              HaoyuTuoPlugin.hideLoadingElement()
              console.log('convert failed, please check your step file');
              appendMsg(`文件${uid}的处理任务失败,请检查step文件格式是否正确`)    
            }
          }).catch((e) => {
            HaoyuTuoPlugin.hideLoadingElement()
            console.log('unknown problems');
            appendMsg(`文件${uid}的处理任务失败,未知的错误!`)
          })
      }

      function uploadFile(file){
        curTime = new Date().getTime()
        processFile(file, curTime)
      }


      function processFile(file, t) 
      {
        if(t !== curTime)
          return

        let uid =  file.name;
        let formData = new FormData();
         
        formData.append("file", file);
        formData.append("uid", uid);  

        appendMsg('开始上传step文件:' + uid)    
        HaoyuTuoPlugin.setLoadingElement('转化中')
        fetch('/step/convert/', {method: "POST", body: formData}).then(result => result.json())
          .then(json => {
            if(json.result === 'ok'){
              appendMsg(`上传文件${uid}成功!`)    
              return getDelayPromise(3000).then(() => getGltfUntilSuccess(uid, t))
            }else if(json.result === 'error'){
              if(json.msg === 'unknown'){
                appendMsg(`上传文件${uid}失败,未知错误!`)    
                throw('unknown error')
              }
              else if(json.msg === 'wrong file format')
              {
                appendMsg(`上传文件${uid}失败,错误的step文件格式!`)    
                throw('wrong step file format')
              }
              else{
                //show message why convert failed

                //try again after 10 seconds
                appendMsg(`服务器忙碌,10秒后重新尝试`)
                return getDelayPromise(10000).then(() => processFile(file, t))
              }
            }
          })
          .catch(err => {
            HaoyuTuoPlugin.hideLoadingElement()
            console.log('we have problem...:', err);
          })

          /*
          if(r.result === 'ok'){
            //show picture
            HaoyuTuoPlugin.setObject('gltf/' + uid + '/' + uid, null, file.name)
          }
          else{
            //HaoyuTuoPlugin.setObject('gltf/' + uid, null, file.name)
            HaoyuTuoPlugin.openMessageDialog(r.msg)
            HaoyuTuoPlugin.hideLoadingElement()

          }
          */ 
      } 


    </script>
  </body>
</html>

stp2browser前端界面操作手册

预览模块界面包含两部分,3D预览视图和工具栏部分。如下图所示:

3D预览视图显示3D模型,工具栏提供多角度或者功能线框图等浏览功能。

3D预览视图提供放缩,旋转,和平移三种预览功能操作。这3种功能实现方式如下:

1. 旋转。采用鼠标操作时,通过鼠标左键在3D预览视图界面通过“点击”和“拖拽”的形式即可实现旋转模型的操作。通过触摸屏操作时,通过单指“触摸”3D预览界面,并“滑动”触摸点即可实现旋转的功能。

2. 放缩。采用鼠标操作时,通过滚动鼠标中键,即可实现放大和缩小模型的功能。采用触摸屏操作时,通过双指“触摸”3D预览界面,并双触摸点做相对或相向“滑动”即可实现放缩功能。

3. 平移。采用鼠标操作时,通过鼠标右键在3D预览视图界面通过“点击”和“拖拽”的形式即可实现平移模型的操作。采用触摸屏操作时,通过双指“触摸”3D预览界面,并双触摸点做平行“滑动”即可实现放缩功能。

这里的工具栏总共有6个按钮,它们分别是:最佳放缩,线框图,正交视图,前视图,左视图,和全屏按钮。如下图所示:

下面分别对这几个功能做详细描述:

1. 最佳放缩。最佳放缩按钮能将当前模型放缩到最佳大小。

2. 线框图。此按钮能够将模型设定为线框图,如下图所示:

3. 正交视图。此按钮能够将预览模型的视角切换到最初打开模型时预览的视角,如下图所表示:

4. 前视图。此按钮切换为前视图

5. 左视图。此按钮切换为左视图

6. 全屏。此按钮将设置3D预览视图为全屏模式

stp2browser 2.0版本升级描述

stp2browser 2.0版本相比于1.0提供了更强大的stp文件信息的解析能力,对预渲染的数据提供了更多的优化细节,专门针对于大文件提供了更强大的优化。与此同时,stp2browser 2.0应用了docker容器,拥有了独立的数据库,具备并行处理和运算队列的管理能力。

性能对比表格

对比项目stp2browser 1.0stp2browser 2.0
http请求数http请求数比较多,尤其对于大文件而言。相比于1.0版本,数量减少一半。
预渲染文件大小不变的曲面处理误差,预渲染文件较大采用更智能的误差取值算法,拥有更小的预渲染文件大小
颜色解析不具备step文件的颜色解析能力具备step文件颜色解析能力
可移植性针对不同版本类型的操作系统需要针对性的安装,可移植性差采用docker容器技术,可以基本无视安装环境差异,可移植性强
拓展性在性能方面拓展性不高由于采用docker容器技术,具备了计算机集群的配置能力。拥有很强的性能拓展能力。
可控性不具备系统资源的控制能力具备系统资源的配置能力,独立性强。可指定占用cpu,内存等资源的大小。不对系统占用过多资源而产生灾难性后果。
并发能力本身不具备并发处理能力可以在两方面处理并发事件:1,条件允许的情况下,可以配置计算机集群增强运算并行处理能力。2,系统自身带有任务队列管理功能。
交互接口常规的http请求的接口,对于大文件的请求,需要保持长时间的http连接。会造成系统资源的浪费,以及与其交互的程序造成不便采用轮询的api接口,对于处理运算api不会保持长时间的http连接。从而减少系统资源的浪费,并增强与第三方程序交互的能力。
日志系统系统泛用日志系统项目针对性的日志系统,便于维护