浏览器中的LibreCAD
摘要
一位开发者借助大型语言模型,将开源2D CAD应用LibreCAD移植到浏览器中运行,使用了WebAssembly和Qt的WebAssembly支持。
暂无内容
查看缓存全文
缓存时间: 2026/07/02 23:10
# LibreCAD 在浏览器中运行 — magik.net
来源:https://magik.net/librecad/
发布于 2026 年 6 月 29 日 · 更新于 2026 年 7 月 1 日
时不时地,我需要快速画一个简单的 2D 图纸,比如平面图。我不是 CAD 用户,也不想为了画五条线和一个圆就去安装一个 CAD 软件。免费的选项都是桌面应用:你得下载、安装、启动,然后在本地工作。这在你自己桌边时没问题,但现在是 2026 年,我觉得不能直接打开一个标签页就画图,尤其还不用登录,这有点傻。
我其实之前从未用过[LibreCAD](https://librecad.org/),也没听说过它,而且我也不是它的目标用户。我只是想要个快速画草图的东西。所以,与其再安装一个桌面应用,我想:找个开源软件,然后在 OpenCode 里直接让 GLM-5.2 把它移植到 WebAssembly。
值得指出的是,整个过程非常省心且“容易”,但这完全归功于 Qt 团队在 Wasm 支持上的巨大投入,以及整个 Wasm 生态系统目前已经相当成熟。
结果发现,这刚好是模型能力的边界,但在一些指导下还是搞定了。如果能原生支持视觉,并具备一些计算机使用能力,模型就能自主调试问题,而 GLM-5.2 缺少这个能力。结果如下:整个应用(不是查看器,不是子集)被编译成 [WebAssembly](https://webassembly.org/),就在这个网站上运行。点击加载(源代码:github.com/magik6k/LibreCAD-Web (https://github.com/magik6k/LibreCAD-Web)):
[启动 LibreCAD →](https://magik.net/librecad/app.html)
首次加载约 18 MB(Brotli 压缩)。之后浏览器会缓存它。需要基于 Chromium 的较新浏览器(Chrome 或 Edge 137+)——移植依赖于 WebAssembly JSPI,Firefox 和 Safari 尚未支持。更多原因见下文。
以下内容完全由 LLM 编写,但粗略看来技术上是准确的,就像这个应用似乎大致能在浏览器中运行,但仔细检查可能包含可怕的 bug。这纯粹是一个 FAFO 风格的不费力的项目,你还能期待什么呢?
## 这是什么
LibreCAD 是一个免费、GPL 许可的 2D CAD 应用。它读写 DXF 和 DWG 文件,支持图层、块、尺寸标注、填充,以及大多数 2D CAD 工具应有的功能。它基于 C++ 和 [Qt](https://www.qt.io/) 构建,自 QCad 时代就已存在。
这次移植通过 [Emscripten](https://emscripten.org/) 和 Qt 官方的 WebAssembly 平台支持,将完全相同的 C++ 源代码编译成 WebAssembly。没有 JavaScript 重新实现,没有 web 原生分支,没有服务器端渲染——真正的桌面应用就在你的浏览器标签页中运行。
## 是如何做到的
前 90% 是机械性的:搭建工具链、编译、启动 GUI、连接文件。最后 10%——让模态对话框真正工作——才是问题的关键所在,也正是它迫使整个工具链重建。以下是真实的版本。
### 工具链和编译
一个基于 Ubuntu 24.04、Emscripten 和 Qt 的 Docker 镜像。完整的 LibreCAD 源代码通过 Qt 自带的 `qt.toolchain.cmake`(原始 Emscripten 工具链文件会让 `find_package(Qt6)` 失败)编译并链接成一个 `.wasm` 二进制文件。纯桌面启动路径——命令行参数解析、启动画面、首次运行对话框、版本检查网络——都用 `#ifndef Q_OS_WASM` 保护起来,桌面构建不受影响。
### 启动 GUI
Qt for WebAssembly 通过 WebGL 渲染,并通过其平台插件传递浏览器事件。主窗口启动,工具栏和停靠面板出现,画布接收鼠标和键盘输入。到目前为止一切顺利。
### 难点:嵌套对话框和 `exec()`
LibreCAD 是一个真正的桌面应用,桌面应用会不断重新进入事件循环:`QDialog::exec()` 会阻塞直到你关闭对话框,下拉列表框会启动自己的循环,从偏好设置对话框打开的取色器会再嵌套一个循环。在网页上,**你不能阻塞主线程**——没有办法“在此处等待”而不冻结页面。所以 `exec()` 根本不会返回。
Emscripten 的答案是 [Asyncify](https://emscripten.org/docs/porting/asyncify.html):它重写二进制文件,使得一次阻塞调用能够展开到浏览器并稍后恢复。Qt 支持它,这也是大多数 Qt-WASM 应用使用的方案。它有效——但只针对 **一层**。Asyncify 一次只能挂起一个调用深度。所以打开一个对话框没问题,但当你点击该对话框内的下拉列表框,或者从应用偏好设置中打开取色器时,第二次挂起无处可去,整个应用就卡住了。对于一个偏好设置充满了下拉框和颜色按钮的 CAD 程序来说,这不是一个小毛病,而是一个无法使用的状态。
解决方案是 [JSPI](https://v8.dev/blog/jspi)(WebAssembly JavaScript Promise Integration):一种原生浏览器挂起机制,与 Asyncify 不同,它可以任意嵌套。Qt 6.9 可以支持它(`-device-option QT_EMSCRIPTEN_ASYNCIFY=2`),但它需要原生 WebAssembly 异常(`-fwasm-exceptions`),而预构建的 Qt 包两者都不包含。因此,该移植现在**从源代码构建 Qt 6.9**,用于 WebAssembly,并启用 JSPI + Wasm 异常。
这引出了真正的难题。JSPI 只允许一个 WebAssembly 栈在它通过一个“承诺(promising)”函数进入时挂起,而 Emscripten 只将 `main()` 标记为这样的函数。但是一旦 `main()` 返回(在 Web 上必须如此),每个浏览器事件——每次打开对话框的点击——都会在自己的新栈上到达,而这个栈**不是**承诺的,所以挂起会因 `trying to suspend without WebAssembly.promising` 而中止。要让其工作,需要三个协调一致的更改:
- 将 Qt 的 DOM 事件处理器注册为一个 `emscripten::async()` embind 函数,这样每个鼠标/键盘事件都在一个承诺的框架内运行(这需要 Emscripten 4.0+;旧版本有一个 embind+JSPI 的 bug,会在启动时中止)。
- 以同样的方式包装 Qt 的定时器和投递事件回调,这样由事件循环触发的挂起也能工作。
- 将 `main()` 重构成异步形式——创建应用并返回;浏览器驱动循环——因为 `-fwasm-exceptions` 与旧的“模拟无限循环”技巧不兼容。
有了这些,`QDialog::exec()`、下拉列表框、嵌套取色器和上下文菜单子菜单都可以工作,且嵌套深度不限。无需应用程序级的对话框重写——平台做了正确的事。
### 让画布变快
第一个可工作的版本在可用窗口大小下以 4–5 fps 的帧率绘制。性能分析将所有帧时间都集中在 Qt 的一个函数上:`blend_untransformed_generic_rgb64`。原因是 Qt 的 WebAssembly 后备存储的像素格式——一个预乘 alpha 的 `RGBA8888` 表面(这正是 HTML 画布需要的)。它不是 Qt 的快路径光栅格式之一,所以**每一次**将绘制内容复制到窗口的操作都回退到通用的 64 位每像素混合,每帧三次。将后备存储切换为预乘的 `ARGB32`(Qt 最优化的格式,与所绘制图层匹配)可以让合成走 SIMD 路径,并在刷新时进行一次格式转换,生成画布需要的 RGBA 字节。帧率大致提高了三倍——这是引擎级别的全面胜利,而不是画布层面的 hack。
### 文件,在没有文件系统的情况下
浏览器没有真正的文件系统,而 Qt 的辅助 API(`getOpenFileContent`/`saveFileContent`)在这个 JSPI 构建中无法可靠地传递字节——打开返回了一个未填充的缓冲区,保存尝试了一个分块可写流选取器,但从未产生下载。两者现在都通过一个薄的 JavaScript shim 来处理:**打开**选取一个文件,在 JS 中读取,然后将字节直接写入 Emscripten 的内存文件系统(MEMFS),然后通过路径加载;**保存**序列化到 MEMFS,然后将字节交给一个 `Blob` 和一个人工下载链接。CAD 字体(47 个 `.lff` 文件)和填充图案被打包成一个 30 MB 的数据包预加载到 MEMFS,应用设置通过 IndexedDB 在重新加载时持久化。
### 生产环境
Brotli 压缩将总传输量从 ~70 MB 降低到大约 **18 MB**。PDF 导出通过 `QPdfWriter`(位于 QtGui 中,因此无需可用的 PrintSupport 模块即可生存)工作,并下载结果。一个自定义的 HTML shell 替换了 Qt 的默认加载器,带有启动画面和进度条,并在你没有 JSPI 支持时显示友好的提示信息。
## 试试看
[启动 LibreCAD →](https://magik.net/librecad/app.html)
加载后,试试这些操作:
- **打开 DXF:**文件 → 打开 → 从你的电脑中选择一个 DXF 文件。如果没有,可以在 [LibreCAD 仓库](https://github.com/LibreCAD/LibreCAD) 中找到示例文件。
- **画线:**点击直线工具,在画布上点击两个点。
- **平移/缩放:**鼠标滚轮缩放,中键拖动平移。
- **保存:**文件 → 另存为 → 你的浏览器会下载 DXF。
- **导出 PDF:**文件 → 打印 → 下载一个 PDF。
## 技术规格
| 项目 | 详细信息 |
|---|---|
| **构建 Qt 版本** | 6.9.3,从源码构建,用于 WebAssembly |
| **挂起后端** | JSPI(`QT_EMSCRIPTEN_ASYNCIFY=2`)+ 原生 Wasm 异常 |
| **Emscripten** | 4.0.7 |
| **基础镜像** | Ubuntu 24.04 (Docker) |
| **C++ 标准** | C++17 |
| **线程** | 单线程 |
| **内存模型** | wasm32 (4 GB 上限) |
| **二进制大小** | librecad.wasm: 39 MB 原始 → 16 MB Brotli;librecad.data (字体+图案): 30 MB 原始 → 2.2 MB Brotli;librecad.js (运行时胶水): 264 KB 原始 → 52 KB Brotli;**总传输量 (Brotli)** **~18 MB** |
| **工作范围** | 打开/编辑/保存 DXF: 是 (浏览器文件选择器+下载);DWG 读取: 是 (已绑定 libdxfrw);模态对话框、下拉框、取色器: 是,可任意深度嵌套 (JSPI);所有绘图工具: 是 (直线、圆弧、圆、多段线、样条线、填充、尺寸标注、文本);所有修改工具: 是 (移动、旋转、缩放、镜像、修剪、倒角、偏移、炸开);图层、块、库插入: 是;SVG 导出: 是;PDF 导出: 是 (QPdfWriter,作为文件下载);设置持久化: 是 (IndexedDB);翻译 (30+ 语言): 是 (已捆绑 .qm 文件);浏览器支持: 基于 Chromium (Chrome/Edge 137+);不支持 Firefox/Safari (JSPI);多窗口 MDI: 仅画布内 (网页无 OS 窗口);打印到物理打印机: 否 (使用浏览器打印 PDF) |
## 源代码
分支托管在 [github.com/magik6k/LibreCAD-Web](https://github.com/magik6k/LibreCAD-Web) 的 `wasm-port` 分支。上游是 [github.com/LibreCAD/LibreCAD](https://github.com/LibreCAD/LibreCAD)。所有更改都通过 `#if defined(Q_OS_WASM)`、`#ifndef LC_NO_PRINT` 和 `#ifndef LC_NO_NETWORK` 保护隔离——桌面构建从同一源码树编译并运行完全一致。
有趣的部分都在平台层,不在 LibreCAD 自己的代码里:对 Qt WebAssembly 后端的一些小补丁(承诺事件处理器、ARGB32 后备存储)、从源码构建 Qt 的配方,以及 JavaScript 文件打开/保存 shim。LibreCAD 本身几乎不需要更改就能让嵌套对话框工作——JSPI 承担了重任。这些 Qt 补丁足够小,值得向上游提议。
## 注意事项
- **目前仅限 Chromium。** 嵌套对话框修复依赖于 WebAssembly JSPI,目前仅在基于 Chromium 的浏览器(Chrome/Edge 137+)中提供。Firefox 和 Safari 正在开发中;当它们支持时,这个构建应该也能运行。
- **首次加载需要几秒钟。** 18 MB 的 WASM + 数据需要下载和编译。之后会被缓存。
- **保存 = 下载。** 浏览器无法写回你打开的文件。“保存”和“另存为”会给你一个下载副本;请手动覆盖到你的磁盘。这是 Web 平台的限制,不是 bug。
- **重新加载后无最近文件。** 最近文件路径是内存中的临时句柄,重新加载后消失。文件列表会被存储,但文件本身不会。请从你的磁盘重新打开。
- **未测试移动浏览器。** 界面是为鼠标+键盘设计的。触摸事件通过 Qt 的输入系统路由,但手机上的工具栏太小了。
## 许可证
LibreCAD 采用 GPL-2.0 许可证。WebAssembly 二进制文件是 GPL-2.0 源代码的编译形式,因此适用相同的许可证。此特定构建的源代码在上文链接的 `wasm-port` 分支中。
---
相似文章
Show HN: 浏览器中的KiCad
KiCad,开源PCB EDA套件,现已通过Emergence Engineering的PCBJam在浏览器中可用,利用WebAssembly、WebGL和多线程仿真实现接近原生的性能。
Show HN: CADara – 我开发了一个开源的浏览器内 CAD 工具
CADara 是一个开源的浏览器端 CAD 工具,让用户可以直接在网页浏览器中创建 3D 模型。
LaTeX.wasm: 浏览器中的LaTeX引擎
SwiftLaTeX 通过 WebAssembly 直接在浏览器中启用 LaTeX 编译,允许用户无需服务器端 LaTeX 安装即可创建和编辑 LaTeX 文档。
《半条命2》在浏览器中运行
《半条命2》已通过 WebAssembly 移植,可直接在网页浏览器中运行,无需安装即可游玩。
llama.cpp的Web UI现在支持通过Web Workers在浏览器中执行模型生成的JavaScript(可选加入)
llama.cpp的Web UI现在支持通过Web Workers在沙箱iframe中执行模型生成的JavaScript,作为可选功能实现轻量级的代理代码执行。