本人对于 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