ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Node.js调用OpenCascade实现BREP几何内核与布尔运算

Node.js调用OpenCascade实现BREP几何内核与布尔运算

简介:node-occ是一套基于OpenCASCADE几何内核的Node.js实体建模扩展,旨在让开发者在JavaScript环境中直接构造BREP实体、执行CSG布尔运算并读写STEP/IGES文件,适用于Web CAD应用、Three.js辅助建模以及几何格式转换等场景。资源包共105个文件,以36个js脚本、22个h头文件和21个cc实现文件为主,js负责API封装与前端示例,h与cc对应V8绑定及几何内核算法,另有JSON配置、bat/yml/makefile/gyp等跨平台构建脚本以及step/stp标准模型样例,压缩包约6.53MB,目录层级清晰,便于按模块阅读和二次编译。已有1921人参与学习。内容覆盖完整封装层、ShapeFactory等核心工具类,并附带可运行Web应用,演示makeBox、makeCylinder、cut、writeSTEP等典型建模流程。读者既可用脚本快速生成三维实体,也能通过源码深入理解Node.js与C++扩展的交互方式,为后续在CAD/CAE系统或三维可视化项目中嵌入实体建模能力提供扎实参考。整体设计兼顾入门学习与工程集成,可显著降低在Web端调用OpenCASCADE实体内核的门槛。

1. 为什么要在 Node.js 里碰 BREP:OpenCascade 移植的底层逻辑

做过 Web CAD 的人大概都遇到过这个尴尬局面:Three.js 画模型画得再漂亮,顶点和三角面片说到底是一堆离散数据,想做个布尔运算、倒个圆角、导出 STEP 文件,前端轮子全得自己造。而 node-occ 这个项目,把 OpenCascade(OCC)这个工业级 BREP 几何内核编译成 Node.js 的原生模块,让服务端可以直接处理边界表示实体——也就是带拓扑信息的精确几何体,而不是网格近似。这意味着浏览器端的 Three.js 负责展示,Node.js 负责造实体和导出 CAD 格式,整条链路不再依赖桌面端程序。适合的人群很明确:正在做在线建模工具、STEP/IGES 文件解析服务、或者想在 Node 里做 CSG 运算的开发者。先说一句反直觉的结论:这东西比你想的好编译,但比你想的更难跑稳,问题几乎全出在原生模块的链接和版本匹配上。

2. 几何内核选型:为什么 OCC 能成为 Node 生态的首选

2.1 BREP 数据结构与 CSG 运算的关系

边界表示(BREP)的核心思路是用「面-边-顶点」的拓扑层级描述一个实体。每个面有几何曲面定义,每条边有曲线定义,面与面之间通过边相连,形成闭合的壳。与网格模型最大的区别在于:BREP 里的球面是一个数学精确的解析曲面,而不是一堆三角形;做布尔运算时,求交得到的是精确的相交曲线,再重新缝合拓扑。CSG(构造实体几何)就是在这个基础上通过布尔并、交、差来组合基本体。node-occ 直接暴露了 OCC 的 BRepAlgoAPI_Fuse、BRepAlgoAPI_Common、BRepAlgoAPI_Cut 这三个核心类,本质上就是把 C++ 的 API 用 emscripten 编译成 wasm 后做了一层 JavaScript 绑定。

理解这个数据结构对后续写代码很关键。你在 node-occ 里创建一个盒子,得到的不是一个包含 12 个三角形的数组,而是一个带有 6 个面、12 条边、8 个顶点的拓扑实体。用shape.faces()拿到的是面对象,可以查它的面积、法向量、所属曲面类型;用shape.edges()拿到的是边对象,可以查曲线类型和长度。这些查询能力是网格模型完全不具备的。一个典型的判断场景:当你需要检测两个零件是否干涉时,如果模型是 BREP,可以直接做布尔交运算看结果是否为空;如果模型是网格,你得先做碰撞检测或体素化,精度和性能都差得多。

2.2 OCC 模块加载方式:WASM 与现代编译链的取舍

node-occ 的编译方案走的是 emscripten 输出 WASM 的路子。安装时会下载对应平台的预编译包,本质是一个.wasm文件加一个 JS 胶水层。加载方式不是require('node-occ')就直接能用,而是需要通过异步初始化拿到实例:

const initOpenCascade = require('node-occ'); async function main() { const occ = await initOpenCascade(); // 创建两个盒子 const box1 = occ.createBox(0, 0, 0, 10, 10, 10); const box2 = occ.createBox(5, 5, 5, 10, 10, 10); // 做布尔并集 const fused = occ.fuse(box1, box2); console.log('结果面的数量:', fused.faces().count()); } main().catch(err => console.error(err));

createBox的前三个参数是左下角坐标,后三个是长宽高;createBox(x, y, z, dx, dy, dz)的数值单位默认是毫米,这与 STEP 文件的标准单位保持一致。fuse返回的是新的拓扑实体,原始的两个盒子不会被修改,这在连续做多次布尔运算时需要注意内存增长问题。初始化函数返回的occ对象是单例,所有几何操作都挂在这个实例上,不需要也不应该重复初始化。如果你在 Electron 渲染进程里用,记得把初始化放到主进程,worker 线程里做运算,否则 WASM 实例的内存管理会出现竞争问题。

2.3 文件格式支持:STEP、IGES 与 JSON 序列化的实际边界

node-occ 支持读写 STEP 和 IGES 这两种工业标准格式。STEP 是当前最通用的 CAD 交换格式,IGES 是老系统留存的格式,两者的解析在 OCC 内部走的是完全不同的翻译器。实际使用中,STEP 的读写稳定性和精度都更好,IGES 经常遇到非有理曲面转有理曲面的退化问题。node-occ 还提供了一组 JSON 序列化方法,可以把实体转成自己的 JSON 格式存储,下次直接读回来重建实体,省去重新建模的耗时。

一个要注意的边界:node-occ 的 STEP 导出是基于STEPControl_Writer的默认配置,导出的文件没有颜色、图层、装配体信息,只有纯几何与拓扑。如果下游需要颜色,你要自己在 STEP 文件头里塞STYLING_ITEM,但 node-occ 的封装没有暴露这个接口,常见做法是导出后用脚本解析补写,或者干脆约定下游只接收纯几何。IGES 导出同理,精度参数走默认值,如果遇到下游软件打开破面,先查导出时有没有报拓扑错误,再考虑改writeIGES的精度参数:

// 读取 STEP 文件并导出为 BREP JSON const fs = require('fs'); const initOpenCascade = require('node-occ'); async function convertStepToJson(stepFile) { const occ = await initOpenCascade(); const shape = occ.readStepFile(stepFile); if (shape === null) { console.error('解析失败,请检查 STEP 文件是否包含实体'); return; } const json = occ.writeBrepJson(shape); fs.writeFileSync('output.json', json); // 验证:重新读回 const restored = occ.readBrepJson(json); console.log('原始实体面数量:', shape.faces().count()); console.log('重建实体面数量:', restored.faces().count()); }

readStepFile返回 null 的情况常见于文件里只有曲线和点云、没有闭合实体的场景。OCC 的默认导入策略是只保留形体数据,忽略产品的装配树,所以你拿到的是一个扁平列表,不是层级结构。writeBrepJson的序列化包含了完整的拓扑和几何信息,体积比对应 STEP 文件大 3 到 5 倍,适合做服务端缓存,不适合做传输格式。

3. 从源码到可用:node-occ 的编译安装与版本匹配

3.1 安装路径:预编译包、源码编译与 Docker 兜底

node-occ 的安装方式有三种,按推荐顺序排:直接走 npm 预编译包、源码编译、Docker 容器里跑。预编译包封装的 OCC 版本和 Node ABI 版本是绑定死的,如果你用的是较新的 Node 大版本,npm install 大概率会触发源码编译,因为预编译包覆盖的 ABI 版本有限。源码编译在这类项目里属于「能做但别轻易碰」的事——你需要完整工具链:Python 3.8+、CMake 3.16+、支持 C++17 的编译器,以及至少 8 GB 内存,编译 OCC 核心库大约要 20 到 40 分钟,过程中还容易因为磁盘空间不足挂掉。

# 走预编译包(Node 14/16/18 LTS 版本推荐) npm install node-occ # 如果预编译包不存在,强制源码编译: npm install node-occ --build-from-source

--build-from-source会用 node-gyp 拉取依赖并编译,日志里如果出现In file included from .../OpenCascade/...说明源码拿到了,接下来就是等。如果这两个都不顺手,Docker 是保底手段:官方镜像里有现成的编译好环境,把项目目录挂载进去跑,宿主机的 Node 版本再乱也不影响容器内的模块加载。但 Docker 方案有两个麻烦:一是容器内的 WASM 模块需要映射端口或共享卷才能和外部通信,二是性能损耗不大但调试复杂度高,文件权限问题会浪费不少时间。

3.2 Node ABI 与 WASM 的兼容性陷阱

node-occ 依赖的是 Node 的 N-API,N-API 是一个稳定的 C 接口层,理论上跨 Node 大版本兼容。但 WASM 内存管理和底层调用走的路径比纯 JS 更复杂,实测下来 Node 20 上跑预编译包偶尔会崩,Node 18 最稳。如果你在 Electron 的环境里使用,Electron 自带 Node 版本可能和 node-occ 预编译时用的 ABI 不同,解决办法是使用 electron-rebuild 重新编译原生模块:

npm install --save-dev @electron/rebuild npx electron-rebuild -f -w node-occ

这个命令会把 node-occ 针对 Electron 的 ABI 重新编译,编译出的.wasm文件体积大约在 30 到 50 MB(取决于 OCC 包含的模块数量),加载时内存占用峰值在 300 MB 左右。注意 Electron 的contextIsolation配置如果开启,native 模块无法直接暴露到渲染进程,必须在 preload 脚本里用ipcRenderer转发调用。还有一个隐性的坑:WASM 实例依赖WebAssembly.Memory,Electron 的sandbox: true会限制 WASM 的大内存申请,表现为初始化偶发失败,报错信息是RangeError: WebAssembly.Instance。遇到这个情况,把相关窗口的 sandbox 关掉,或者把运算放到 Node 子进程里跑。

3.3 验证安装成功的最低探测脚本

安装完不要急着写业务代码,先把下面这个 6 行的脚本跑通。它做了三件事:初始化内核、创建实体、查询拓扑信息,如果这三步都通过,说明 native 模块和 WASM 加载链路是完整的:

const initOpenCascade = require('node-occ'); initOpenCascade().then(occ => { const box = occ.createBox(0, 0, 0, 100, 50, 20); const info = { faces: box.faces().count(), edges: box.edges().count(), volume: box.volume() }; console.log(JSON.stringify(info, null, 2)); // 期望输出:faces=6, edges=12, volume=100000 }).catch(err => { console.error('初始化失败:', err); process.exit(1); });

volume方法返回的是实体的体积,单位立方毫米,100 * 50 * 20 = 100000,这个结果可以用来快速验证坐标轴方向是否正确。OCC 的右手坐标系是 Y 轴向上的,和 Three.js 的 Y 轴向上一致,但和部分机械设计软件里的 Z 轴向上不同;如果你的前端模型需要转换坐标系,在导出的 STEP 里做旋转而不是在前端做旋转,这样下游得到的数据才是正确的。跑通这个脚本后,再去碰业务代码,能省掉一大半的排查时间。

4. 用 node-occ 做实际建模:从基本体到复杂布尔运算的完整脚本

4.1 基本体创建与参数体系

node-occ 的基本体封装覆盖了 OCC 的 BRepPrimAPI_MakeBox、MakeCylinder、MakeSphere、MakeCone、MakeTorus 这几个常用类。参数体系上,createBox最简单,直接给坐标和尺寸;createCylinder需要半径、高度、还有旋转轴方向;createSphere除了半径还能指定角度范围做部分球。这些参数和 OCC C++ API 的构造函数参数顺序保持一致,对照过 OCC 文档的话基本不用费劲猜。

const initOpenCascade = require('node-occ'); async function createPrimitives() { const occ = await initOpenCascade(); // 圆柱:半径 20,高 60,坐标原点在底面中心 const cylinder = occ.createCylinder(20, 60); // 球体:半径 15,默认生成完整球 const sphere = occ.createSphere(15); // 圆环:主半径 30,管半径 5 const torus = occ.createTorus(30, 5); // 锥体:底面半径 25,顶面半径 10,高 40 const cone = occ.createCone(25, 10, 40); const entities = [cylinder, sphere, torus, cone]; entities.forEach((s, i) => { console.log(`实体 ${i}: 面数=${s.faces().count()}, 体积=${s.volume().toFixed(2)}`); }); } createPrimitives();

createCylinder的默认方向是 Z 轴,如果要做水平轴线的圆柱,你需要用一个变换矩阵把它旋转过去,或者绕一圈封装:先创建,再用occ.rotate(shape, axis, angle)转到目标方向。volume 输出带两位小数,对精度要求高的场景要用绝对误差来判断运算结果,不要用浮点相等。还有一个细节:createSphere的角度参数单位是弧度,不是角度,常见用法是createSphere(15, 0, Math.PI)创建半球。

4.2 布尔运算组合与细节控制

布尔运算是 BREP 建模中最容易出问题的环节,但也是最有价值的部分。三个核心操作对应 OCC 底层的BRepAlgoAPI_Fuse、BRepAlgoAPI_Cut、BRepAlgoAPI_Common。实际使用时,你需要了解一个关键参数:fuzzy tolerance。OCC 在布尔运算时,如果两个实体表面的距离小于这个容差值,会被视为重合。默认值是1e-7毫米,这在实际建模中经常过于严格,导致应该被判定为共面的两个面被当成相交来处理,生成细碎边或退化面。

const initOpenCascade = require('node-occ'); async function booleanOperations() { const occ = await initOpenCascade(); // 基体:120x80x30 的板 const base = occ.createBox(0, 0, 0, 120, 80, 30); // 切除体:中心位置的圆柱孔 const hole = occ.createCylinder(15, 40); const holeCentered = occ.translate(hole, 60, 40, -5); // 差集:打孔 const cutResult = occ.cut(base, holeCentered); console.log('差集后的面数量:', cutResult.faces().count()); // 附加体:右上角的凸台 const boss = occ.createBox(90, 50, 30, 30, 30, 20); const fusedResult = occ.fuse(cutResult, boss); console.log('并集后的面数量:', fusedResult.faces().count()); // 验证体积变化 const originalVolume = base.volume(); const finalVolume = fusedResult.volume(); console.log('原始体积:', originalVolume, '最终体积:', finalVolume); } booleanOperations();

occ.translate返回的是一个新实体,源实体不会被修改。cut的语义是从第一个参数里减去第二个参数与它的交集,和布尔差集的习惯一致;cut后原实体和新实体如果出现共面的情况,OCC 会自动合并或分裂面,这个过程产生的面数量变化是正常现象。如果你发现结果里有面积接近零的面或者极短的边,多半是输入几何有微小重叠区域,把 fuzzy tolerance 调大到1e-5可以改善。这个参数在 node-occ 的 API 外层没有暴露,但可以通过修改底层源码的BRepAlgoAPI_Cut构造参数实现——要动编译源码,复杂度高,一般不建议业务代码里这么搞。

4.3 变换矩阵与装配场景

建模逻辑中几乎一定会遇到实体的平移、旋转、镜像和缩放。OCC 的变换基于gp_Trsf变换矩阵,在这个矩阵上叠加变换顺序:先旋转再平移和先平移再旋转的结果完全不同。node-occ 把常用变换暴露成独立方法,但你需要理解矩阵左乘的顺序语义:

const initOpenCascade = require('node-occ'); async function transformShape() { const occ = await initOpenCascade(); // 在原点创建螺栓头:六角柱 const hexHead = occ.createPrism(occ.createRegularPolygon(6, 15), 8); // 平移到 (100, 50, 20) const translated = occ.translate(hexHead, 100, 50, 20); // 绕着 X 轴旋转 45 度(注意:旋转原点在坐标原点) const rotated = occ.rotate(translated, [1, 0, 0], Math.PI / 4); // 以点 (100, 50, 20) 为缩放中心,各轴缩放比例不同 const scaled = occ.scale(rotated, 100, 50, 20, 1.5, 1.0, 0.8); console.log('原始体积:', hexHead.volume()); console.log('缩放后体积:', scaled.volume()); // 导出 STEP 检查 occ.writeStepFile(scaled, 'bolt-head.step'); } transformShape();

createRegularPolygon(6, 15)创建的是正六边形的线框,createPrism沿法线方向拉伸成实体;这两个 API 组合起来类似 OCC 的BRepPrimAPI_MakePrism。rotate方法三个参数的顺序是旋转轴向量和角度,角度旋转的正方向遵循右手定则:X 轴旋转正方向是 Y 转向 Z。缩放操作设计为接受缩放中心和三个轴的缩放因子,这个语义与 CAD 软件里的非均匀缩放一致,代价是实体内部的曲面类型会从解析曲面退化为 B 样条曲面。如果后续拿这个实体做布尔运算,性能会明显下降——B 样条曲面的求交复杂度比解析曲面高一个量级。

4.4 从线框到实体:创建自定义轮廓的建模路径

node-occ 支持通过边创建面、通过面创建壳、通过壳创建实体的完整建模链路。最常见的自定义轮廓建模方式:先给一组二维坐标点,用makeEdgeWires创建线框,再用makeFace填充成面,最后用prism拉伸成体。这个流程对应 OCC 的BRepBuilderAPI_MakeWire、BRepBuilderAPI_MakeFace、BRepPrimAPI_MakePrism,是唯一在 node-occ 里做非规则形状的正统路径:

const initOpenCascade = require('node-occ'); async function createCustomShape() { const occ = await initOpenCascade(); // 定义 L 形截面的关键点(二维坐标,XY 平面) const points = [ [0, 0], [40, 0], [40, 10], [15, 10], [15, 30], [0, 30], [0, 0] ]; // 用点数组创建线框 const wire = occ.makeWireFromPoints(points); // 确保线框闭合:首尾相连才有面 const isClosed = wire.isClosed(); console.log('线框闭合状态:', isClosed); if (!isClosed) { console.error('线框未闭合,无法创建面'); return; } // 创建面并拉伸为实体 const face = occ.makeFace(wire); const solid = occ.prism(face, 0, 0, 50); console.log('实体面数量:', solid.faces().count()); occ.writeStepFile(solid, 'l-shape.step'); } createCustomShape();

makeWireFromPoints自动把点数组按顺序连成边,最后一个点会和第一个点连接;如果输入的点数组首尾不重合,它会自动闭合,这既是便利也是隐患。正常建模的复杂轮廓建议自己控制轮廓的坐标顺序,尤其是带圆弧的轮廓——node-occ 不支持直接通过三点做圆弧,你需要先创建圆弧边再连接成线框。makeFace会自动识别线框内的孔洞,如果你的轮廓是带通孔的,需要先创建外轮廓线框和内孔线框,再把它们放进一个makeFace里组合成带孔的面。

5. 常见问题与避坑排查:五条真实踩坑记录

5.1 初始化缓慢或内存暴涨

现象:加载 node-occ 消耗 1 到 2 秒,服务端实测内存占用从 200 MB 直接跳到 500 MB,初始化频繁时可能出现内存溢出。

原因:OCC 的 WASM 实例在初始化时会预加载整个内核数据和默认资源表,这部分内存是常驻的。在多个子进程同时初始化的情况下,内存占用线性递增。

解决:服务端使用单例模式,进程启动时只初始化一次,后续所有请求复用同一个实例。在 Node 子进程的场景下,用一个常驻 worker 承载 OCC 实例,不要每来一个请求就 fork 一个新进程。

5.2 布尔运算结果出现破面或丢失实体

现象:两个实体做布尔交运算,结果返回 null,或者结果实体的面数量明显少于预期,导出 STEP 后下游软件显示破面。

原因:输入实体的精度不一致。圆柱体底面圆的离散精度与另一个实体的曲面精度不匹配时,布尔运算的交线退化,导致生成的边不能正确闭合。

解决:对圆柱类实体,createCylinder内部生成的是精确解析曲面,但如果接收来自 STEP 文件导入的实体,其曲面可能是 B 样条。统一在运算前调occ.convertToBSpline或occ.convertToAnalytic让所有输入实体使用同一类曲面表示。

5.3 导出 STEP 文件后 AutoCAD 无法打开

现象:node-occ 导出的 STEP 文件在开源查看器里正常,但在桌面软件里直接报文件损坏或无法解析。

原因:STEP 文件头的版本字段和产品名称字段不符合部分软件对 STEP AP203 或 AP214 的严格校验。OCC 的默认导出器生成的是 AP203 格式,但部分软件要求 AP214 才支持颜色与图层属性,碰到严格校验的程序就会拒绝。

解决:在 node-occ 的writeStepFile前后补充文件头修改。常见做法是导出后用文本处理替换FILE_SCHEMA字段,例如把('CONFIG_CONTROL_DESIGN')改成('AP214'),同时补上PRODUCT_DEFINITION_CONTEXT的字段。手动改文件头是玄学味道很重的操作,但确实能解决硬兼容问题。

5.4 前端渲染结果与后端几何不一致

现象:Three.js 渲染出来的模型和用 STEP 导出的模型尺寸对不上,或者在浏览器里量出来的长度和实际相差 10 到 25 倍。

原因:单位不一致。node-occ 默认采用毫米,但前端 Three.js 如果按 1 单位 = 1 米约定建模,双方数值 идеологически 相差 1000 倍。更常见的场景:从 STEP 文件读入的实体本身单位是英寸。

解决:读取 STEP 时用occ.readStepFile后调用occ.scale做单位换算,统一到毫米后再进入建模流程。在前端导出 STEP 前反方向缩放回原单位。没有其他稳妥方案,靠前端渲染缩放欺骗眼睛会导致下游加工尺寸全错。

5.5 连续做多次布尔运算后内存持续增长

现象:一次会话里做 20 次以上的布尔运算,内存占用只增不减,最终掉进 GC 陷阱或直接内存溢出。

原因:每次布尔运算的中间结果实体都保存在 WASM 堆里,node-occ 的 JS 引用计数无法感知到 WASM 内部实体的生命周期。

解决:对不再使用的实体显式调用occ.deleteShape释放 WASM 堆内存。这个 API 是 node-occ 对外提供的但很多人没用,内部的引用计数不是 JS 层面的,必须手动管理:

// 每次布尔运算完成后,清理中间实体 const temp = occ.fuse(box1, box2); const result = occ.cut(temp, hole); occ.deleteShape(temp); // 释放中间实体 // 做完业务后清理最终实体 occ.deleteShape(result);

循环引用场景要特别小心:如果实体 A 和实体 B 已经组合成实体 C,删掉 A 和 B 是安全的,它们内部的数据已经被 C 复制走。但如果你删掉了 C,又去访问 A 或 B 的拓扑信息,会触发未定义行为——一般是崩溃而不是报错。

6. 用 Three.js 渲染 BREP 实体:网格化与显示参数调优

BREP 实体没法直接被 Three.js 渲染,需要先转成网格。node-occ 提供了shapeToMesh方法,本质上调用 OCC 的BRepMesh_IncrementalMesh离散化算法。这个算法把精确曲面按指定精度切成三角形,切得越密渲染越平滑,但文件体积和前端加载时间会指数增长。

关键参数有两个:linearDeflection控制面片与真实曲面的最大弦偏差,angularDeflection控制相邻面片法向量夹角的允许偏差。默认值前者 0.1 毫米、后者 0.5 弧度,对精细曲面来说偏粗,建议调到 0.01 毫米和 0.2 弧度:

const initOpenCascade = require('node-occ'); const THREE = require('three'); async function shapeToThreeMesh(stepFile) { const occ = await initOpenCascade(); const shape = occ.readStepFile(stepFile); // 网格化参数:弦偏差 0.05mm,角偏差 0.3 弧度 const meshData = occ.shapeToMesh(shape, 0.05, 0.3); // meshData 包含 position 数组和 index 数组 const geometry = new THREE.BufferGeometry(); geometry.setAttribute('position', new THREE.Float32BufferAttribute(meshData.positions, 3)); geometry.setIndex(meshData.indices); geometry.computeVertexNormals(); return geometry; }

shapeToMesh返回的对象包含positions和indices两个数组,前者是长度 3 的倍数的浮点数组,后者是三整数索引。computeVertexNormals计算出来的法向量是平滑后的,适合看渲染效果;但如果要做模型分析的标注或剖切,你需要原始面的法向量,那要在 node-occ 层面取face.normal()而不是用前端计算的结果。网格转化逻辑的耗时与精度参数强相关,精度提高 10 倍,面片数量大约增加 30 倍,耗时会从毫秒级跳到秒级。生产环境建议做一个参数决策函数:只有用户放大查看时才动态生成高精度网格,缩略图级别用默认精度就行。

前端显示的模型和 BREP 实体是「一个用于看、一个用于算」,不要试图在前端做精确交互操作。测量、布尔、切片这些操作全部回传服务端,在 BREP 实体上计算完后把结果网格化再推回前端。早期版本我在前端做了很多交互,结果陷入三角网格与 BREP 数据来回转换的泥潭,从那以后我每次做 Web CAD 架构都强制走一遍「编辑在服务端、显示在前端」的链路,有效隔离两边的数据模型差异。如果你打算用这个项目做在线建模工具,建议第一版就把这个边界定清楚,后面会省很多调试时间。希望帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进