FreeCAD 浏览器版上线,无需安装即可使用
速览
FreeCAD 推出浏览器版本,用户无需安装即可在网页端使用。该版本采用WebAssembly技术,实现了完整的CAD建模功能。这意味着设计师和工程师可以随时随地通过浏览器访问FreeCAD,降低了使用门槛。
AI 深度解读
背景
FreeCAD 是一款开源、参数化 3D CAD 建模软件,支持草图约束、实体建模、嵌入式 Python 脚本以及超过 17 个工作台(如 Part、Sketcher、PartDesign、FEM 等)。它由约 150 万行 C++ 和 70 万行 Python 代码构成,规模远超同类工具。此前,已有开发者成功将 LibreCAD(一个完整的 Qt 桌面应用)和 OpenSCAD 通过 WebAssembly 编译到浏览器中运行,这一模式依赖于 Qt for WebAssembly、JSPI(JavaScript Promise Integration)、静态依赖栈以及对线程、文件系统和 OpenGL 假设的全面审计。FreeCAD 的移植自然成为下一个挑战——它比前两者大一个数量级,但仅用约 4 天时间便从 Hacker News 讨论走到了可运行状态。
核心内容
移植工作主要由 AI 代理 Fable 完成,目的是在大型复杂项目中测试 Fable 的能力。所有 48 条提示词以及完整的会话记录(包括子代理和工作流)均已公开。FreeCAD 在浏览器中首次加载约 96 MB(Brotli 压缩),后续由浏览器缓存。需要基于 Chromium 的浏览器(Chrome/Edge 137+),因为 Firefox 和 Safari 尚未支持 WebAssembly JSPI。启动后,一个包含圆柱体切割的盒子演示文档会在实时 3D 视口中打开。
FreeCAD 并非单一程序,而是一个平台。底层组件包括:Qt GUI、OpenCASCADE(OCCT,工业级 B-rep 几何内核)、Coin3D 与 Quarter(基于 Open Inventor 的场景图,驱动 3D 视图)、完整的嵌入式 CPython 3.14、PySide6/shiboken6(Qt 的 Python 绑定),以及针对 FEM 工作台交叉编译的 VTK 9.3 子集和 Salome SMESH 网格划分器。此外还有 17 个以上工作台。所有组件被静态编译为单个 196 MB 的 WebAssembly 模块,位于 Qt 6.11 旁边。
其中两个环节此前从未在浏览器中实现:PySide6 和 pivy(Coin 的 Python 绑定)在 WebAssembly 下运行;以及将 OCCT、Coin、CPython、PySide、VTK、SMESH 链接到一个模块中。
移植细节
JSPI 的必要性:LibreCAD 可以使用预构建的 Qt for WebAssembly,但 FreeCAD 不能,因为 FreeCAD 大量使用模态对话框(185 个 C++ exec() 调用,156 个 Python 调用),阻塞的 Python 代码无法机械地重写为回调。JSPI 允许在对话框打开时挂起 WebAssembly 栈,从而保持 QDialog::exec() 语义。这需要 -feature-wasm-jspi,因此 Qt 必须从源码编译(Qt 6.11.1 for wasm,使用 JSPI 和原生 wasm 异常,基于 Emscripten 4.0.12),并附带手建静态前缀的 OCCT、CPython、ICU、Boost、Xerces、{fmt} 和 yaml-cpp。
内核优先策略:先验证 OCCT + CPython + FreeCAD App 层在 WebAssembly 中运行,再处理 GUI。FreeCAD 的 C++ 模块是 Python 扩展模块(PyMOD_INIT_FUNC),通常运行时通过 dlopen 加载。但 WebAssembly 模块没有 dlopen,因此每个模块被静态链接并在 Py_Initialize 之前通过 PyImport_AppendInittab 注册到解释器的 inittab 中。配合 home 路径、Python 环境和用于 node 端测试的 NODERAWFS 构建,FreeCADCmd 首先在 Node 中无头运行(打开 .FCStd 文件、重新计算、STEP 往返),随后在浏览器中运行。
GUI 链接:将完整 GUI 目标链接起来主要是簿记工作——共享的 inittab、QProcess 替代方案(浏览器中无子进程)、保护 Start 模块的时区/进程假设。有趣的部分是 OpenGL。FreeCAD 通过 Coin3D 渲染,Coin3D 使用 1990 年代的固定函数 OpenGL(矩阵栈、glBegin/glVertex、glLightfv、客户端顶点数组)。WebGL2 不支持这些,因此引入了一个手写的固定函数 GL 到 GLES2 的垫片(与 OpenSCAD 相同的解决方案,但针对 Coin 更大的 GL 表面)。
GUI 启动:链接的模块不等于能运行的模块。GUI 启动面临两个问题。第一,C++ 静态初始化顺序:当所有东西在一个模块中,OCCT、Coin、Python 和 Qt 的全局构造函数按未设计的顺序执行,有些读取了后面设置的状态。第二,Qt-wasm 平台插件需要特定的导出函数和一个能实际挂起的事件循环。早期的 Qt 消息处理器揭示了阻塞点——事件循环需要 asyncify 来存活 app.exec()。之后,主窗口(文档树、属性编辑器、工具栏、菜单、模态对话框)成功出现。
硬币渲染器:Coin 的渲染器有约 70 个 glBegin 调用,分布在 20 个文件中,还有矩阵栈数学。作者构建了一个约 900 行的固定函数仿真器(WasmGLFixedFunc),实现 Coin 使用的 GLES 3.0 子集:CPU 端矩阵栈、一个带双光源逐像素着色的 GLSL ES 程序、立即模式批处理。最棘手的障碍是合成:Qt 通过将 GL 部件的帧缓冲纹理包装到顶层窗口的上下文中来合成,但 WebGL 上下文不共享。修复方法是使用离屏帧缓冲(WasmGLWidget),场景渲染到其中,再读回并交给 Qt 的普通光栅合成。
随后的一系列 bug:实体渲染不可见——GL 仿真器将 glGetDoublev(GL_DEPTH_CLEAR_VALUE) 存根为 0,导致深度缓冲清除到近平面,所有深度测试失败;一行默认值修复。大量 INVALID_ENUM 错误——Coin 查询遗留固定函数枚举(通过 glGetFloatv/glGetBooleanv),WebGL 拒绝,通过本地拦截和回答解决。选择拖动时零件变白——橡皮筋覆盖层抓取了空白帧缓冲,改为在覆盖层下保留实时场景。顶点数组快速路径——Coin 的 isSupported 门要求所有十个遗留客户端数组入口点解析,因此添加了无操作的 glIndexPointer/glArrayElement;然后 SoVertexArrayIndexer 需要 glDrawRangeElements 和 glMultiDrawElements(WebGL2 缺乏),通过仿真器路由。最隐蔽的问题是仿真器自身的临时 VBO 在绘制后仍保持绑定,导致后续收集时将仿真器缓冲当作 Coin 的缓冲,从错误内存读取顶点——场景渲染为点阵而非三角形。通过在仿真器缓冲集合中跟踪其缓冲并在快照绑定时忽略它们,顶点数组路径从约 1.3 fps 的立即模式变为可交互。
异常迁移:构建曾使用 Emscripten 的 ASYNCIFY 加基于 JavaScript 的异常。迁移到 JSPI 加原生 wasm 异常时遇到 WebAssembly 的棘手问题:wasm 异常有两种编码——传统的 try/catch 指令和更新的 try_table/exnref——V8 拒绝混合使用这两种编码的模块。整个 C++ 世界统一使用新编码基本上可以解决,但 ...
(原文
