本人对于 BIM 一无所知,笔记是搜索引擎搜索 猜测 总结结果,有些结论和断言会有不准确和不专业的地方,请原谅。
背景
工作项目需要在页面上显示 BIM。
BIM 有很多解释,指的是 ,简单地说,它是为建筑工程绘制的 2D/3D 模型。
功能要求是:将提供 .rvt 显示文件模型 web 页面中。
经过一番研究:
- BIM 模型文件有多种格式,
.rvt格式是 Autodesk Revit 开发的项目文件。 - 不能直接使用前端
.rvt文件,three.js 可加载.rvt文件转化的.json模型内容的文件显示。 - 要将
.rvt转化成.json(实际上是一堆.json使用其他资源文件) revit 例如,付费软件 Autodesk Revit,试用版 … - Revit 支持二次开发,包括转换文件 API,似乎用 ASP.NET 但不是一键转换,似乎需要一些操作和知识(不懂)。
- 一些平台实现了二次开发,并提供上传、转换、显示等一站式服务例如,广联达 BIMFACE、Autodesk 官方团队的 Autodesk Forge(本人主要研究的后者)。
.rvt很难找到测试文件。.json很难找到文件。我没有找到它。测试文件由客户提供,因此不方便提供。Autodesk Forge 官方提供了一些示例文件,但我没有成功下载,你可以试试: Revit 示例项目文件
Forge
指引
Autodesk Forge 提供了很多 API 也可用于模型数据的开发和管理 Autodesk Forge Viewer 在页面上加载显示模型。
Autodesk Forge 的工作人员提供了一个 《Autodesk Forge中文帮助中心包括教程文档,Forge 阅读高级、常见问题、在线工具等内容是最快的 Forge 建议至少阅读完方法 《Autodesk Forge 学习简谈系列。
这个系列被称为 新手案例教程包括视频教程《Forge Viewer从构建到部署,以及视频中的参考文档,我不会重复具体的构建和演示。建议自己构建,熟悉简单的过程。
流程
我跟着 Nodejs 案例再次构建,直到页面成功显示,我的需求基本满足,所以我没有继续看以后的扩展。
案例的一般流程是:
1、注册登录
官方注册账号并登录,开始试用
2、身份验证
创建 App,主要用于获取开发 Client ID 和 Client Secret,基于它们生成具体的访问范围权限 token,调用 API 时需将 token 传递过去。
3、数据管理
Forge 云中存储数据的名称 bucket 在容器(抽象)中。
- 首先创建一个 bucket
- 然后上传支持格式的模型文件,例如
.rvt - 然后执行转换操作,将模型文件转换为 Forge Viewer 可加载的数据格式。
4、页面展示
Forge 可任意支持的模型文件转换为模型文件 SVF 格式(.svf ),.svf 文件记录了模型中使用的资源,可以知道需要加载哪些文件,包括 .json、图片、.gz、.bin 等。
案例中通过 Viewer 加载远程的 svf 然后加载相关资源文件,最后渲染整个模型。
svf 包提取器
关于 SVF
SVF 它不是一个单一的文件,而是一个包括构建集合信息和属性包的数据包
.svf清单文件(二维模型).f2d)。而 Forge Viewer 的 JavaScript 对此数据进行分析和渲染。目前 SVF 没有文档描述数据格式,也没有官方端口直接下载数据包。但是,这些文件可以根据清单文件下载。
注意:SVF2 暂时不支持下载数据包。
Forge Viewer 支持 SVF 格式为内部格式,作为入口 .svf 文件实际上就是个压缩包,可以通过压缩软件解压。其中包含的 manifest.json 文件记录了模型所需的相关资源的文件地址,例如 .json、图片、.gz、.bin 等。
下图是 {3D}.svf 文件解压结果:

功能
基本的显示需求已经通过构建案例实现,但实际上转换的数据已经存储在中 Forge 云服务器对数据管理和网络要求不友好,需要下载到自己的私人服务器进行存储。
可是 Forge 没有提供傻瓜式的一键下载 SVF 包的功能。
幸运的是,官方开发人员提出了一个可行的计划,即根据获得的文件清单批量手动下载和打包《Autodesk Forge 学习简谈 - 4》。
团队中也有人(PHILIPPE LEEFSMA)从废弃的官方在线工具中解耦 的功能:《Forge SVF Extractor in Node.js》
官方还开发了支持一键下载的支持方式 vscode 也可以参考源码开发自己的下载功能。
方案
.svf 文件及其记录的相关资源统称为 SVF,Forge 称呼这些转换的结果是 - SVF 衍生(或衍生)文件。
通过 DerivativesApi 和 URN 可提取模型 manifest 存储该模型的数据 Derivatives 信息,然后通过 Derivatives 手动拼接信息中路径的官方地址是 Viewer 加载的文件地址。
整合这些衍生文件的所有路径,批量下载并存储在本地 SVF 包的下载。
URN:Uniform Resource Name(统一资源名称)不同 URL标准格式(统一资源定位符)URI,以唯一的名义指向资源,而不是资源的所在地。请参考更多信息《HTTP 权威指南》中 URN 的介绍。
代码
本人使用的 PHILIPPE 解耦代码,并对代码进行了一些调整,使其能够直接使用:
- 删除代码中保留的一些 PHILIPPE 本项目中的模块
- 一些 API 过时使用导致调用报错
- 由于某些路径分隔符不一致错误报告
- 我把代码放在里面 Nodejs 所以改用环境运行 CommonJS 加载模块的方式
- 环境搭建和 token 生成沿用 Learn Forge 案例
最终结果:
// ExtractorSvc.js const archiver = require('archiver') const { DerivatiesApi } = require('forge-apis') const request = require('request') const mkdirp = require('mkdirp') const Zip = require('node-zip') const Zlib = require('zlib') const path = require('path') const fs = require('fs') const _ = require('lodash') // 转化路径中的分隔符 // path 会将路径中的路径分隔符全部转化为当前环境的默认格式,可能会是 `\`,在拼接其它路径时可能会有问题,这里进行了统一替换 function formatSep(str, sep = path.sep) { const reg = new RegExp(sep === '/' ? '\\\\' : '/', 'g') return str.replace(reg, sep) } // Extractor Service 类 class ExtractorSvc { // 构造函数 constructor() { // 初始化 API 实例 this.derivativesAPI = new DerivativesApi() } // 实例名成 name() { return 'ExtractorSvc' } /** * 【对外提供的 API】 * 将 SVF 文件全部下载到服务器,并返回所有文件的路径 * @param {AuthClient} oauth2Client 案例中定义的 getClient 方法生成的 token * @param {AuthToken} credentials 案例中定义的 getInternalToken 方法生成的 token * @param {string} urn 案例中 new ObjectsApi().getObjects API获取的 objectId * @param {string} directory 存储资源的目标绝对路径 * @returns */ download(oauth2Client, credentials, urn, directory) { return new Promise(async (resolve, reject) => { // mkdirp:mkdir 的 promise 分装 // 创建目标目录,确保目录确实存在 await mkdirp(directory) // 根据 URL 获取顶层的 manifest const manifest = await this.derivativesAPI.getManifest(urn, { }, oauth2Client, credentials) // 整合要获取的全部资源 const derivatives = await this.getDerivatives(oauth2Client, credentials, manifest.body) // 格式化资源信息,提取必要字段 const nestedDerivatives = derivatives.map(item => { return item.files.map(file => { const localPath = formatSep(path.resolve(directory, item.localPath)) return { basePath: item.basePath, guid: item.guid, mime: item.mime, fileName: file, urn: item.urn, localPath } }) }) // 将多维数组拍平,转化为一维数组 const derivativesList = _.flattenDeep(nestedDerivatives) // 为每个资源文件创建异步下载任务 const downloadTasks = derivativesList.map(derivative => { return new Promise(async resolve => { // 由于要拼接 HTTP 地址,所以强制使用 `/` 分隔符 const urn = formatSep(path.join(derivative.basePath, derivative.fileName), '/') // 下载每个文件 const data = await this.getDerivative(oauth2Client, credentials, urn) const filename = formatSep(path.resolve(derivative.localPath, derivative.fileName)) // 保存文件 await this.saveToDisk(data, filename) resolve(filename) }) }) // 等待所有文件下载 const files = await Promise.all(downloadTasks) resolve(files) }) } // 解析 manifest 全部要获取的资源,提取 guid mime 和解析URN生成的路径信息对象 parseManifest(manifest) { const items = [] const parseNodeRec = node => { const roles = [ 'Autodesk.CloudPlatform.DesignDescription', 'Autodesk.CloudPlatform.PropertyDatabase', 'Autodesk.CloudPlatform.IndexableContent', 'leaflet-zip', 'thumbnail', 'graphics', 'preview', 'raas', 'pdf', 'lod' ] if (roles.includes(node.role)) { const item = { guid: node.guid, mime: node.mime } // 解析 URN 生成的路径信息对象 const pathInfo = this.getPathInfo(node.urn) items.push(Object.assign({ }, item, pathInfo)) } if (node.children) { node.children.forEach(child => { parseNodeRec(child) }) } } parseNodeRec({ children: manifest.derivatives }) return items } // 收集 SVF 资源 getSVFDerivatives(oauth2Client, credentials, item) { return new Promise(async (resolve, reject) => { try { const svfPath = item.urn.slice(item.basePath.length) // 记录 svf 文件路径 const files = [svfPath] // 通过 request 获取 SVF 文件信息(Buffer) const data = await this.getDerivative(oauth2Client, credentials, item.urn) // 将 SVF Buffer 数据转化成压缩包对象格式 const pack = new Zip(data, { checkCRC32: true, base64: false }) // 获取里面的 manifest.json 信息 const manifestData = pack.files['manifest.json'].asNodeBuffer() const manifest = JSON.parse(manifestData.toString('utf8')) // 如果 manifest 还记录了资源文件,则一并记录 if (manifest.assets) { manifest.assets.forEach(asset => { // 跳过 SVF 嵌入资源 if (asset.URI.indexOf('embed:/') === 0) { return } files.push(asset.URI) }) } return resolve( Object.assign({ }, item, { files }) ) } catch (ex) { reject(ex) } }) } // 收集 F2D 资源(示例文件中没有此类型,所以不做介绍) getF2dDerivatives(oauth2Client, credentials, item) { return new Promise(async (resolve, reject) => { try { const files = ['manifest.json.gz'] const manifestPath = item.basePath + 'manifest.json.gz' const data = await this.getDerivative(oauth2Client, credentials, manifestPath) // 解压缩 Gzip const manifestData = Zlib.gunzipSync(data) const manifest = JSON.parse(manifestData.toString('utf8')) if (manifest.assets) { manifest.assets.forEach(asset => { // 跳过 SVF 嵌入资源 if (asset.URI.indexOf('embed:/') === 0) { return } files.push(asset.URI) }) } return resolve( Object.assign({ }, item, { files }) ) } catch (ex) { reject(ex) } }) } // 整合顶层 manifest 中要获取的资源 getDerivatives(oauth2Client, credentials, manifest) { return new Promise(async (resolve, reject) => { // 解析整合 manifest 全部资源的必要信息 const items = this.parseManifest(manifest) const derivativeTasks = items.map(item => { // 根据 mime 处理 switch (item.mime) { case 'application/autodesk-svf': // 如果是 SVF 则获取文件并解析文件中的资源地址列表 return this.getSVFDerivatives(oauth2Client, credentials, item) case 'application/autodesk-f2d': // 如果是 F2D 则获取文件并解析文件中的资源地址列表 return this.getF2dDerivatives(oauth2Client, credentials, item) case 'application/autodesk-db': // 固定 gz 文件 return Promise.resolve( Object.assign({ }, item, { files: [ 'objects_attrs.json.gz', 'objects_vals.json.gz', 'objects_offs.json.gz', 'objects_ids.json.gz', 'objects_avs.json.gz', item.rootFileName ] }) ) default: // 其它类型文件,如 jpg 等 return Promise.resolve( Object.assign({ }, item, { files: [item.rootFileName] }) ) } }) const derivatives = await Promise.all(derivativeTasks) return resolve(derivatives) }) } // 解析 URN 生成路径相关信息 getPathInfo(encodedURN) { const urn = decodeURIComponent(encodedURN) const rootFileName = urn.slice(urn.lastIndexOf('/') + 1) const basePath = urn.slice(0, urn.lastIndexOf('/') + 1) const localPathTmp = basePath.slice(basePath.indexOf('/') + 1) const localPath = localPathTmp.replace(/^output\//, '') return { rootFileName, localPath, basePath, urn } } // 通过 URN 获取资源数据(Buffer) getDerivative(oauth2Client, credentials, urn) { return new Promise(async (resolve, reject) => { // 拼接官方固定的地址 const baseUrl = 'https://developer.api.autodesk.com/' // url 是拼接好的文件资源路径 // URN包含文件路径信息,可能有中文,需要编码 const url = baseUrl + `derivativeservice/v2/derivatives/${ encodeURIComponent(urn)}` // 使用 request 请求资源文件 request( { url, method: 'GET', headers: { // 注意添加 token Authorization: 'Bearer ' + credentials.access_token, 'Accept-Encoding': 'gzip, deflate' }, encoding: null }, (err, response, body) => { if (err) { return reject(err) } if (body && body.errors) { return reject(body.errors) } if ([200, 201, 202].indexOf(response.statusCode) < 0) { return reject(response) } return resolve(body || { }) } ) }) } // 将文件写入到本地 saveToDisk(data, filename) { return new Promise(async (resolve, reject) => { // 创建原有的文件所在目录 await mkdirp(path.dirname(filename)) // 创建写入流 const wstream = fs.createWriteStream(filename) const ext = path.extname(filename) wstream.on('finish', () => { resolve() }) // 写入数据 if (typeof data === 'object' && ext === '.json') { wstream.write(JSON.stringify(data)) } else { wstream.write(data) } wstream.end() }) } /** * 【对外提供的 API】 * 将指定的文件全部打包成一个 zip 压缩包 * @param {*} rootDir 要打包的文件所在目录(绝对路径) * @param {*} zipfile 生成压缩包的完整绝对路径 * @param {*} zipRoot 压缩包中根目录文件名 * @param {*} files 要打包的文件地址列表 * @returns */ createZip(rootDir, zipfile, zipRoot, files) { // 统一路径分隔符 rootDir = formatSep(rootDir) zipfile = formatSep(zipfile) zipRoot = formatSep(zipRoot) return new Promise((resolve, reject) => { try { // 创建写入流 const output = fs.createWriteStream(zipfile) //生成 archiver 对象,打包类型为zip const archive = archiver('zip') // 写入流关闭(打包完成)即决议 output.on('close', () => { resolve() }) archive.on('error', err => { reject(err) }) // 将打包对象与写入流关联 archive.pipe(output) if (files 标签:国产smd铝电解电容rvt