☰
Node.js 中基于 OpenCascade 的 BREP 实体建模实战
2026/10/20 8:31:44 网站建设 项目流程

简介:node-occ 是一套面向 Node.js 开发者的 OpenCascade 扩展,用于在服务端构建 BREP 实体与 CSG 布尔运算,适合具备一定 JavaScript 基础、希望把 CAD 建模能力接入 Web 或自动化流程的工程师。它通过 V8 包装器把 OpenCascade 的几何内核暴露为简洁的 JavaScript API,可完成立方体、圆柱等基本体的创建,以及 cut、fuse 等布尔操作,并支持导出 STEP 文件,示例 Web 应用也一并提供。资源包共 105 个文件,约 6.53MB,以 36 个 js 脚本、22 个 C++ 头文件与 21 个 cc 实现文件为主,另有 json 配置、bat 构建脚本、yml 流水线、step/stp 模型样例及 md 说明文档,覆盖从源码编译到模型输出的完整链路。目前已有 1923 人学习下载。借助 ShapeFactory、Mesh、Solid、Face 等模块,读者可快速理解几何内核的封装方式,掌握在 Node.js 中生成、切割与导出三维实体的方法,为 CAD 工具链与 threejs 可视化集成提供可复用的基础。

1. 当 JavaScript 遇上 BREP:为什么要在 Node.js 里做实体建模

如果你做过前端三维可视化,大概率用过 Three.js 那套基于网格(Mesh)的渲染管线。三角形面片堆出来的模型看着挺像回事,但一旦要算体积、求交线、做布尔运算,立刻就露馅——网格模型没有拓扑信息,两个面片“贴”在一起不代表它们真的相交。这就是 BREP(Boundary Representation,边界表示)存在的意义:用参数化曲面和精确曲线描述实体的边界,保留点、线、面之间的拓扑关系,让几何运算有严格的数学基础。

node-occ 这个方向要解决的问题很具体:把 OpenCascade(OCCT)这套工业级 BREP 几何内核,通过 Node.js 的 N-API 绑定暴露给 JavaScript 生态。你可以在服务端用 JS 写脚本,调用 OCCT 的布尔运算、倒角、抽壳、STEP/IGES 读写能力,输出精确的 BREP 实体或三角化后的网格。适合谁?做 CAD 工具链的、搞参数化建模服务的、需要在 Web 后端批量处理三维模型的团队。它不适合只想画个旋转立方体的场景——那种用 Three.js 就够了。

2. 环境搭建与 OCCT 绑定:从源码到可调用的模块

2.1 为什么不能直接 npm install 了事

OCCT 是一个超过百万行 C++ 代码的几何内核,node-occ 这类绑定项目通常不会把整个 OCCT 编译产物塞进 npm 包。常见做法是:你本地先编译好 OCCT 的静态库或动态库,然后 node-occ 的 binding.gyp 或 CMake 配置去链接这些库。这意味着环境搭建是整个流程里最容易翻车的一步。

我一般会先把 OCCT 编译成共享库(.so / .dylib / .dll),而不是静态库。原因很简单:静态库链接进 Node 原生模块后,调试符号丢失严重,一旦出现段错误你连栈都看不全。共享库虽然部署时多几个文件,但排错成本低得多。

2.2 编译 OCCT 的最小命令集

以下是在 Linux 环境下编译 OCCT 的典型步骤。注意 OCCT 版本迭代较快,具体 CMake 选项名可能随版本微调,以你实际拉取的源码为准。

# 假设源码在 /opt/occt-src,安装到 /opt/occt-install mkdir -p /opt/occt-build && cd /opt/occt-build cmake /opt/occt-src \ -DCMAKE_BUILD_TYPE=Release \ -DCMAKE_INSTALL_PREFIX=/opt/occt-install \ -DBUILD_MODULE_Visualization=OFF \ -DBUILD_MODULE_ApplicationFramework=OFF \ -DBUILD_MODULE_DataExchange=ON \ -DBUILD_MODULE_ModelingAlgorithms=ON \ -DBUILD_MODULE_FoundationClasses=ON \ -DUSE_FREETYPE=OFF \ -DUSE_TBB=OFF make -j$(nproc) make install

逻辑说明:BUILD_MODULE_Visualization=OFF关掉可视化模块,因为 node-occ 的场景是服务端几何计算,不需要 OpenGL 渲染上下文,关掉能省大量编译时间和依赖。DataExchange=ON必须开,STEP/IGES 读写全靠它。ModelingAlgorithms是布尔运算、倒角、抽壳的核心模块。USE_TBB=OFF关掉并行计算支持,除非你确定运行环境有 TBB 且需要多线程加速——单线程在大多数批量处理场景下够用,还少一个依赖。

参数怎么改:如果你需要处理大型装配体,把USE_TBB=ON并确保系统装了 TBB。如果只需要基础几何运算不需要文件交换,DataExchange=OFF能进一步瘦身。

2.3 Node 原生模块的链接配置

node-occ 的 binding 层通常用 node-gyp 或 cmake-js 构建。关键是在 binding.gyp 里正确指定 OCCT 的头文件路径和库文件列表。

# binding.gyp 片段(Python 字典语法,node-gyp 使用) { "targets": [{ "target_name": "node_occ", "sources": ["src/addon.cpp", "src/shape_wrapper.cpp"], "include_dirs": [ "/opt/occt-install/include/opencascade" ], "libraries": [ "-L/opt/occt-install/lib", "-lTKernel", "-lTKMath", "-lTKBRep", "-lTKPrim", "-lTKBO", "-lTKBool", "-lTKSTEP", "-lTKIGES", "-lTKTopAlgo" ], "cflags_cc": ["-std=c++17", "-fexceptions"], "cflags_cc!": ["-fno-exceptions"] }] }

逻辑说明:include_dirs指向 OCCT 头文件目录,注意 OCCT 的头文件是平铺在opencascade目录下的,不是按模块分子目录。libraries里TKernel和TKMath是基础,TKBRep提供 BREP 数据结构,TKPrim是基本体素(Box、Cylinder 等),TKBO和TKBool是布尔运算,TKSTEP/TKIGES是文件读写,TKTopAlgo是拓扑算法。少链接一个库,编译能过但运行时加载会报 undefined symbol。

cflags_cc!里的-fno-exceptions必须去掉,因为 OCCT 内部大量使用 C++ 异常做错误处理,Node 原生模块也需要异常传播。这个坑我踩过:编译通过,一调用布尔运算就进程崩溃,查了半天才发现是异常被禁用了。

2.4 验证绑定是否成功

编译完成后,写一个最小测试脚本确认 OCCT 内核真的被调起来了。

// test_binding.js const occ = require('./build/Release/node_occ'); // 创建一个 10x10x10 的立方体 const box = occ.makeBox(10, 10, 10); console.log('Box created:', box.isValid()); // 创建一个半径 5 的球体 const sphere = occ.makeSphere(5); console.log('Sphere created:', sphere.isValid()); // 布尔差集:立方体减去球体 const result = occ.cut(box, sphere); console.log('Cut result valid:', result.isValid()); console.log('Volume:', result.volume());

逻辑说明:makeBox和makeSphere是 OCCTBRepPrimAPI_MakeBox和BRepPrimAPI_MakeSphere的封装。isValid()对应BRepCheck_Analyzer的检查结果。cut对应BRepAlgoAPI_Cut。volume()通过GProp_GProps计算。如果这一步能跑通并输出合理的体积值(立方体减球体后体积应小于 1000),说明绑定链路是通的。

3. 用 JS 构建 BREP 实体:从基本体素到布尔运算

3.1 基本体素的创建与参数含义

OCCT 提供了一套标准体素构造 API,node-occ 通常会把这些封装成 JS 函数。以下是最常用的几个及其参数。

JS 函数对应 OCCT 类关键参数注意事项
makeBox(dx, dy, dz)BRepPrimAPI_MakeBox三轴长度原点在角点,不是中心
makeSphere(r)BRepPrimAPI_MakeSphere半径原点在球心
makeCylinder(r, h)BRepPrimAPI_MakeCylinder半径、高度轴向默认 Z 轴
makeCone(r1, r2, h)BRepPrimAPI_MakeCone底半径、顶半径、高r2=0 时为尖锥
makeTorus(r1, r2)BRepPrimAPI_MakeTorus主半径、管半径r2 必须小于 r1

这些体素创建出来就是精确的 BREP 实体,不是网格。一个makeSphere(5)返回的球体,其表面是解析曲面,任意点的法线可以精确计算,体积是 4/3πr³ 的精确值(浮点精度内)。

3.2 布尔运算的三种模式与性能差异

布尔运算是 BREP 建模的核心。OCCT 提供并集(Fuse)、差集(Cut)、交集(Common)三种。node-occ 封装后通常叫fuse、cut、common。

const occ = require('./build/Release/node_occ'); // 创建两个重叠的立方体 const box1 = occ.makeBox(10, 10, 10); const box2 = occ.makeBox(10, 10, 10); // 平移 box2 使其与 box1 部分重叠 const moved = occ.translate(box2, 5, 5, 5); // 并集 const fused = occ.fuse(box1, moved); console.log('Fuse volume:', fused.volume()); // 应为 1000 + 1000 - 125 = 1875 // 差集 const cut = occ.cut(box1, moved); console.log('Cut volume:', cut.volume()); // 应为 1000 - 125 = 875 // 交集 const common = occ.common(box1, moved); console.log('Common volume:', common.volume()); // 应为 125

逻辑说明:两个 10³ 立方体在 (5,5,5) 方向重叠,重叠区域是 5³=125。并集体积 1875,差集 875,交集 125。这三个值可以用来验证布尔运算是否正确。

性能上,布尔运算的耗时与参与运算的面数呈超线性关系。两个简单立方体做布尔运算在毫秒级,但两个各含上千面的实体做布尔运算可能耗时数秒甚至更久。常见优化手段是:先做包围盒粗判,不相交的直接跳过;对复杂实体先做面片化预处理,减少参与精确求交的面数。

3.3 变换操作与拓扑保留

平移、旋转、缩放是建模中最频繁的操作。OCCT 的变换通过gp_Trsf类实现,node-occ 通常封装成translate、rotate、scale。

// 绕 Z 轴旋转 45 度 const rotated = occ.rotate(box1, [0, 0, 1], Math.PI / 4); // 非均匀缩放(注意:非均匀缩放可能破坏某些曲面的参数化) const scaled = occ.scale(box1, 2, 1, 1); // 镜像 const mirrored = occ.mirror(box1, [1, 0, 0]); // 关于 YZ 平面镜像

逻辑说明:rotate的第二个参数是旋转轴向量,第三个是弧度。scale接受三个缩放因子,分别对应 XYZ。非均匀缩放对平面和圆柱面是安全的,但对球面、圆环面等双曲率曲面,非均匀缩放后不再是标准解析曲面,OCCT 会将其转换为 NURBS 表示,精度和性能都会受影响。如果后续还要做布尔运算,尽量在变换前完成,或者变换后重新检查isValid()。

3.4 从 BREP 到网格:三角化的参数控制

服务端建模的最终产物往往需要传给前端渲染,而前端渲染需要网格。OCCT 提供BRepMesh_IncrementalMesh做三角化,node-occ 通常封装成mesh或tessellate。

// 三角化,线性偏差 0.1,角度偏差 0.5 弧度 const meshData = occ.tessellate(shape, 0.1, 0.5); console.log('Vertex count:', meshData.vertices.length / 3); console.log('Triangle count:', meshData.indices.length / 3); // meshData.vertices 是 Float32Array,每三个一组 // meshData.indices 是 Uint32Array,每三个一组构成一个三角形

逻辑说明:tessellate的第一个参数是线性偏差(Linear Deflection),控制网格顶点到实际曲面的最大距离。第二个是角度偏差(Angular Deflection),控制相邻面片法线夹角的最大值。这两个值越小,网格越精细,顶点数越多。0.1 的线性偏差对大多数可视化场景够用,如果模型尺寸很大(比如建筑级),需要按比例放大这个值,否则网格会爆炸。

注意:三角化后的网格丢失了 BREP 拓扑信息,不能再用于精确布尔运算。如果需要同时保留 BREP 和网格,建议在 BREP 上做完所有精确操作后,最后一步再三角化。

4. 文件读写与数据交换:STEP、IGES 和 STL 的取舍

4.1 STEP 读写:工业交换的首选格式

STEP(.step/.stp)是 ISO 10303 标准,支持 BREP 实体的精确交换。OCCT 的STEPControl_Reader和STEPControl_Writer是底层实现。

// 读取 STEP 文件 const shape = occ.readSTEP('/path/to/model.step'); if (!shape || !shape.isValid()) { throw new Error('STEP read failed or shape invalid'); } // 写入 STEP 文件 occ.writeSTEP(shape, '/path/to/output.step');

逻辑说明:readSTEP内部调用STEPControl_Reader::ReadFile和TransferRoots。读取后务必用isValid()检查,因为 STEP 文件可能包含拓扑错误或单位不一致。writeSTEP默认输出 AP214 协议,如果需要 AP203 可以在封装层加参数。

常见坑:不同 CAD 系统导出的 STEP 文件单位可能不同(毫米、英寸、米),OCCT 读取后不会自动转换。如果发现模型尺寸差了几个数量级,先检查单位。另一个坑是 STEP 文件中的颜色和图层信息,OCCT 默认不读取,需要额外调用XCAF相关 API。

4.2 IGES 的局限性与适用场景

IGES(.iges/.igs)是较老的标准,对 BREP 实体的支持不如 STEP 完整。它更擅长表达曲面和曲线,但对实体的拓扑关系描述较弱。如果拿到的 IGES 文件包含的是曲面片而非实体,OCCT 读取后得到的是Shell或Face集合,不能直接做布尔运算,需要先缝合(Sewing)成实体。

// 读取 IGES const shape = occ.readIGES('/path/to/model.iges'); // 如果是曲面片,尝试缝合 const solid = occ.sew(shape, 1e-6); // 缝合容差 1e-6 if (solid.isValid()) { console.log('Sewing succeeded, now a solid'); }

逻辑说明:sew对应BRepBuilderAPI_Sewing,容差参数决定多大间隙内的边会被合并。容差太小缝不上,太大可能把不该合并的边也合并了。1e-6 是常用起点,根据模型精度调整。

4.3 STL 导出:只适合最终展示

STL 是纯网格格式,没有 BREP 信息。导出 STL 意味着你放弃了所有精确几何能力,得到的只是一堆三角形。

// 先三角化再导出 STL const meshData = occ.tessellate(shape, 0.05, 0.3); occ.writeSTL(meshData, '/path/to/output.stl', 'binary');

逻辑说明:writeSTL的第三个参数指定二进制或 ASCII 格式。二进制体积小、读写快,生产环境一律用二进制。线性偏差 0.05 比可视化用的 0.1 更精细,因为 STL 常用于 3D 打印,表面质量要求更高。

注意:STL 导出前必须三角化,且三角化参数直接决定 STL 的精度。如果发现导出的 STL 表面有明显的棱角,先调小线性偏差和角度偏差。

5. 避坑与排查:BREP 建模中那些让人抓狂的瞬间

5.1 布尔运算后实体无效(isValid 返回 false)

现象:两个看起来正常的实体做布尔运算,结果isValid()返回 false,后续操作全部失败。

原因:最常见的是面片自交或容差冲突。OCCT 的布尔运算基于容差(Tolerance),每个顶点、边、面都有自己的容差。如果两个实体的容差设置不匹配,或者运算过程中产生了小于容差的微小边,就会导致拓扑无效。

解决:先对参与运算的实体做fixShape(对应ShapeFix_Shape),统一容差。然后在布尔运算后再次fixShape。如果还不行,尝试调整 OCCT 的模糊容差(Fuzzy Tolerance),在布尔运算 API 中传入一个略大于默认容差的值。

// 修复实体 const fixed1 = occ.fixShape(box1); const fixed2 = occ.fixShape(box2); // 带模糊容差的布尔运算 const result = occ.cut(fixed1, fixed2, { fuzzyValue: 1e-5 });

5.2 内存泄漏:Node 进程跑着跑着就 OOM

现象:批量处理模型时,Node 进程内存持续增长,最终被系统杀掉。

原因:OCCT 的 C++ 对象不受 V8 垃圾回收管理。每次调用makeBox或cut都会在 C++ 堆上分配内存,如果 JS 包装层没有正确释放,就会泄漏。

解决:node-occ 通常会在 JS 对象被 GC 时触发 C++ 析构,但这依赖于正确的引用计数或FinalizationRegistry。如果发现泄漏,检查是否在循环中创建了大量临时 shape 而没有让它们失去引用。手动释放的方式通常是调用shape.delete()或让包装对象离开作用域。

// 批量处理时,每处理完一个模型就释放 for (const file of files) { const shape = occ.readSTEP(file); const result = occ.cut(shape, tool); occ.writeSTEP(result, outputPath); // 显式释放 shape.delete(); result.delete(); }

5.3 三角化后网格出现破洞或法线翻转

现象:tessellate输出的网格在渲染时出现黑色破洞,或者光照方向反了。

原因:OCCT 三角化时,如果面的朝向不一致(有些面法线朝内,有些朝外),生成的三角形绕序就会混乱。另外,如果线性偏差设得太大,曲率大的区域可能三角化失败,留下空洞。

解决:先对实体做orientShape(对应ShapeFix_Solid的朝向修复),确保所有面法线朝外。然后调小线性偏差,特别是对包含小特征(小孔、小圆角)的模型。

// 修复朝向 const oriented = occ.fixOrientation(shape); // 更精细的三角化 const meshData = occ.tessellate(oriented, 0.01, 0.2);

5.4 STEP 读取后单位不对导致尺寸差 1000 倍

现象:读取一个 STEP 文件,发现模型尺寸要么巨大要么极小。

原因:STEP 文件头中定义了长度单位,但 OCCT 的STEPControl_Reader默认不自动缩放。如果文件用米为单位,而你的后续操作假设是毫米,就会差 1000 倍。

解决:读取后检查shape的包围盒尺寸,与预期对比。如果不对,手动缩放。更稳妥的做法是在读取时指定单位转换。

const shape = occ.readSTEP(file); const bbox = occ.boundingBox(shape); const size = bbox.max - bbox.min; // 如果预期是 100mm 左右但实际是 0.1,说明文件单位是米 if (size < 1 && size > 0.001) { const scaled = occ.scale(shape, 1000, 1000, 1000); // 使用 scaled 继续 }

5.5 并发调用导致 OCCT 内部状态冲突

现象:在 Node 的异步回调或多线程环境中同时调用 OCCT 函数,出现随机崩溃或结果错乱。

原因:OCCT 的某些全局状态(如单位系统、消息处理器)不是线程安全的。虽然 OCCT 本身支持多线程,但需要显式配置。Node 的原生模块如果没做好线程隔离,并发调用就会出问题。

解决:最简单的方式是串行化所有 OCCT 调用,用一个队列保证同一时刻只有一个操作在执行。如果性能不够,考虑用worker_threads每个线程加载独立的 OCCT 实例,但这会显著增加内存占用。

// 简单的串行队列 class OccQueue { constructor() { this.queue = []; this.running = false; } async run(fn) { return new Promise((resolve, reject) => { this.queue.push({ fn, resolve, reject }); this.process(); }); } async process() { if (this.running || this.queue.length === 0) return; this.running = true; const { fn, resolve, reject } = this.queue.shift(); try { resolve(await fn()); } catch (e) { reject(e); } this.running = false; this.process(); } }

6. 进阶技巧:用 OCCT 的倒角与抽壳做参数化零件

倒角(Fillet)和抽壳(Shell)是 BREP 建模里最能体现“精确”二字的操作。网格模型做倒角只能靠细分逼近,而 BREP 做倒角是真正在边上生成过渡曲面。

先看倒角。OCCT 的BRepFilletAPI_MakeFillet可以对指定边做等半径或变半径倒角。

// 对立方体的所有边做半径 1 的倒角 const box = occ.makeBox(20, 20, 20); const edges = occ.getEdges(box); // 返回所有边的索引 const filleted = occ.fillet(box, edges, 1.0); console.log('Fillet valid:', filleted.isValid()); console.log('Volume after fillet:', filleted.volume());

逻辑说明:getEdges返回边的列表,fillet的第三个参数是半径。倒角后体积会略微减小(角被削掉了)。如果倒角半径大于相邻面的最小尺寸,倒角会失败,isValid返回 false。这时候需要减小半径或只对部分边做倒角。

抽壳更微妙。BRepOffsetAPI_MakeThickSolid可以把实体变成薄壁件,需要指定要移除的面。

// 把立方体变成开口盒子:移除顶面,壁厚 2 const box = occ.makeBox(30, 30, 30); const faces = occ.getFaces(box); // 找到法线朝 +Z 的面(顶面) const topFace = faces.find(f => { const normal = occ.faceNormal(f); return normal[2] > 0.99; }); const shelled = occ.shell(box, [topFace], 2.0); console.log('Shell valid:', shelled.isValid());

逻辑说明:shell的第二个参数是要移除的面列表,第三个是壁厚。壁厚必须小于实体的最小半尺寸,否则抽壳后自交。如果抽壳失败,先检查壁厚是否过大,再检查要移除的面是否与相邻面有相切关系——相切面抽壳容易出问题。

一个实用的参数化零件例子:带安装孔的支架。先做一个底板,再在四角做圆柱孔,然后对边缘倒角,最后抽壳减重。

// 底板 let bracket = occ.makeBox(100, 60, 10); // 四角安装孔 const holePositions = [[10,10], [90,10], [10,50], [90,50]]; for (const [x, y] of holePositions) { const hole = occ.makeCylinder(3, 20); const moved = occ.translate(hole, x, y, -5); bracket = occ.cut(bracket, moved); } // 边缘倒角 const edges = occ.getEdges(bracket); bracket = occ.fillet(bracket, edges, 1.5); // 底面抽壳(移除底面,壁厚 2) const faces = occ.getFaces(bracket); const bottomFace = faces.find(f => { const normal = occ.faceNormal(f); return normal[2] < -0.99; }); bracket = occ.shell(bracket, [bottomFace], 2.0); occ.writeSTEP(bracket, '/path/to/bracket.step');

这段代码的每一步都可能失败,所以生产环境里我会在每步之后加isValid检查,失败就回退到上一步并记录日志。倒角半径 1.5 和壁厚 2 是经验值,实际要根据板材厚度和加工能力调整。

验证方法:把输出的 STEP 文件重新读入,检查体积和包围盒是否符合预期。更严格的做法是用 OCCT 的BRepCheck_Analyzer做完整拓扑检查,确保没有自由边、没有无效面。

我自己的习惯是:任何涉及布尔运算和倒角的脚本,先在单个模型上跑通并人工检查结果,再批量处理。批量处理时一定要加超时和内存限制,因为一个畸形模型可能让 OCCT 陷入死循环。这个习惯帮我省了无数次深夜排查的麻烦。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询