# stlkit 与现有网格库的对比

## 定位

trimesh 是 **Python** 生态的通用三角网格库，需要 Python 解释器和 numpy。

stlkit 是用 **MoonBit** 写的 3D 打印前置检查工具链，编译成原生代码 / wasm / JS，
不依赖任何运行时，回答一个问题：**这个模型能不能 3D 打印。**

两者面向的场景不同，下面逐项说清楚。

---

## 一、trimesh 是什么

按它自己的 README 原话：

> Trimesh is a pure Python 3.10+ library for loading and using triangular meshes
> with an emphasis on watertight surfaces.

- 唯一硬依赖是 numpy；scipy / networkx / lxml / pyglet 等是「装了才有更多功能」的软依赖
- 支持导入 STL、OBJ、OFF、PLY、GLTF/GLB、3MF、XAML、3DXML 等
- 支持导出 GLB/GLTF、STL、PLY、OFF、OBJ、COLLADA 等
- 还做 DXF/SVG 矢量路径、OpenGL 预览、Jupyter 内嵌 three.js 预览

能力包括：凸包、截面与切片、质量属性（体积 / 质心 / 转动惯量 / 惯性主轴）、
布尔运算（Manifold3D 或 Blender）、体素化、拉普拉斯平滑、细分、
射线求交、有向包围盒 / 包围球 / 包围柱、最近点与有符号距离、
按连通块拆分（`mesh.split()`）、欧拉数（`mesh.euler_number`）、
绕向与法线修复、四边/三角洞修补。

这是一套通用网格处理工具集，而 stlkit 只做「打印前检查」这一段。

---

## 二、和 trimesh 的逐项对比

每一条都把两边摆在一起。**结论不藏**：第 10 条（网格运算）是我们明显不如它的，
其余各有取舍——第 5、6、8、9 条是我们这边更全，第 1、2、3、4、7 条是两边都能做、
只是做法不同。

### 1. 语言和运行时

- **trimesh**：Python 3.10+，硬依赖 numpy
- **stlkit**：纯 MoonBit，编译成原生代码 / wasm / JS，无 FFI、无运行时依赖

### 2. 能跑在哪

- **trimesh**：需要 Python 解释器。进不了浏览器，进不了没有解释器的独立二进制，也进不了 MoonBit 项目
- **stlkit**：编译成 native 是单文件可执行程序；编译成 wasm 在浏览器里跑（在线试用页面的分析就是它，不是 JavaScript 重写的）；也能作为 MoonBit 库被别的项目引用

### 3. 支持的输入格式

- **trimesh**：STL、OBJ、OFF、PLY、GLTF/GLB、3MF、XAML、3DXML 等
- **stlkit**：STL（ASCII + 二进制）、OBJ、3MF、PLY（ASCII + 两种字节序）

### 4. 支持的输出格式

- **trimesh**：GLB/GLTF、STL、PLY、OFF、OBJ、COLLADA 等
- **stlkit**：二进制 / ASCII STL、OBJ、PLY

### 5. 网格质量校验

- **trimesh**：水密性、凸性、绕向一致性、欧拉数
- **stlkit**：水密性、**非流形边、退化面、法线朝向**、绕向一致性、欧拉数

「非流形边」和「退化面」是 3D 打印最常见的两类坏，trimesh 的校验清单里没有单列。

### 6. 质量属性

- **trimesh**：体积、质心、转动惯量、惯性主轴
- **stlkit**：体积、表面积、包围盒、定向包围盒、重心、转动惯量、主惯量、零件数、欧拉数

### 7. 大文件

- **trimesh**：整个网格进 numpy 数组（它要做的布尔运算、射线求交本来就需要完整的拓扑和几何索引）
- **stlkit**：同样支持全量模式；另有一条流式路径，不建网格、单遍算完

实测 95.37 MB、200 万三角形：

| 方式 | 耗时 | 内存 |
| --- | --- | --- |
| 全量解析（保留网格，能校验能修复） | 18088 ms | 293.73 MB |
| 流式扫描（文件整块读） | 2048 ms | 141.22 MB |
| 流式扫描（**分块读**） | 3334 ms | **46.1 MB** |

省 84.3%，每个三角形从 154 字节降到 24 字节，分块模式下文件完全不进内存，
只有一个 256 KB 的缓冲区在循环复用。

trimesh 把网格留在内存里是它的功能决定的——布尔运算和射线求交本来就要求
完整的拓扑和几何索引。而「几百 MB 的扫描件，我只想知道有没有破洞」这个
具体场景，可以不用把整个网格装进来。

### 8. 输出形态

- **trimesh**：库。`mesh.is_watertight` 返回 True / False，判断留给调用方
- **stlkit**：工具链（库 + 命令行 + 网页）。报告里写「切片软件分不清哪里是实心哪里是空的，直接打印会失败」，`--strict` 退出码可直接挂进 CI

### 9. 壁厚检查

- **trimesh**：没有。它提供射线查询（`mesh.ray`），但「壁够不够厚」这个判断要自己搭
- **stlkit**：有。从表面往里打射线，量到对面有多远

壁太薄是 3D 打印最常见的失败原因之一，而这个问题**从外面看模型完完整整，看不出来**。
验算：空心盒子（外壳 10³、内腔 6³）量出 **2**，正好是壁厚；圆环量出 3.95，正好是管子直径。

### 10. 网格运算

- **trimesh**：布尔运算（交 / 并 / 差）、体素化、拉普拉斯平滑、细分，都有
- **stlkit**：都没有

这些服务的是「对网格做变换和运算」，和「判断能不能打印」是两条线，所以没有做。

### 11. 代码来源和许可

- **trimesh**：MIT 许可的 Python 库
- **stlkit**：Apache-2.0。**参考了 trimesh 的设计但没有复制任何代码**

零件数（`split()`）、欧拉数（`euler_number`）、重心（`center_mass`）、
转动惯量（`moment_inertia`）、主惯量（`principal_inertia_components`）、
有向包围盒（`bounding_box_oriented`）、射线求交（`mesh.ray`）、
最近点（`nearest.on_surface`）、PLY 读写——这几项的 API 形状是照着它定的。
语言和数据结构都不同，借鉴的是「一个网格库该有哪几项能力、数据怎么组织」这个层面。
逐条对应关系也写在 README 里。

---

## 三、为什么用 MoonBit 重写是有意义的

**这场黑客松里已经通过初审的项目，全都是「把别的语言里已经成熟的东西用 MoonBit 重写一遍」：**

| 项目 | 它实现的规范 | 其他语言里的成熟实现 |
| --- | --- | --- |
| `moon-robots` | RFC 9309 robots.txt | Python 标准库 `urllib.robotparser` |
| `moon-sfv` | RFC 9651 结构化字段值 | PyPI `http-sfv` |
| `dbc-toolkit` | CAN DBC | PyPI `cantools` |
| `nmea-toolkit` | NMEA 0183 | PyPI `pynmea2` |

（上面四个 Python 实现都实际查过，确实存在。）

「别的语言里已经有成熟实现」这件事，对上面每一个项目同样成立。
它们的价值不在于世界首创，而在于 **MoonBit 生态里从零到一**——
用 MoonBit 的项目没法直接调用 `pip install trimesh`。

而 3D 网格处理这一块，**MoonBit 生态目前是空的**：

- 检索了 mooncakes 全部 2439 个包，逐个检查 15 个 3D 相关候选
- 它们**全是渲染方向**：`mizchi/three` 是 three.js 的 FFI 绑定（模型解析是
  `extern "js"` 交给浏览器里的 three.js 做的，MoonBit 侧没有解析代码）；
  `mizchi/mesh3d` 是渲染原语，OBJ 解析器逐面展开顶点、不留拓扑
- **没有任何一个包做三角网格质量验证**

trimesh 是 Python 的。它进不了 MoonBit。

---

## 四、从 trimesh 借鉴的部分

下面几项是看了 trimesh 的设计之后补的：

**零件数**（对应 trimesh 的 `mesh.split()`）。一个「看起来是一个模型」的文件
里如果有 5 块互不相连的几何，打出来就是 5 个零散的东西。这类问题从预览图上
看不出来，模型画出来是完整的。实现上按**面的邻接**数连通块——
只有共用边才算连接，两个零件只碰一个角不算，因为那种接触在切片时是断的。

**欧拉示性数**（对应 trimesh 的 `mesh.euler_number`）。χ = V − E + F。
水密网格上它是有拓扑含义的：2 = 一个实心块，0 = 有一个穿透的洞，
−2 = 有两个。`examples/torus.3mf` 是个圆环，算出来正好是 **0**。
实现上只在结论是水密时才报——不水密的时候这个数只是一堆计数的组合，
报出来会让人以为读出了什么。

**PLY 读写**（对应它的 PLY 导入导出）。PLY 是**三维扫描仪最常用的输出格式**，
这一点对 stlkit 特别有意义：扫描件动辄几百 MB，正是流式扫描要解决的那种文件。
读支持 ASCII 和两种字节序的二进制，写默认走二进制（和 trimesh 的默认一致）。

导出时会**先焊接顶点**——我们的 Mesh 是三角形列表，一个立方体存 36 个顶点；
PLY 是共享顶点表格式，只该写 8 个。去重用的量化方式和校验、流式扫描是同一套，
「什么算同一个点」整个项目只有一个答案。

写这个解析器时撞上一个真实案例：trimesh 自带的 `fuze_ascii.ply`，**头里声明每个面
15 个值、实际数据每行 21 个**——文件自己前后不一致。按声明一路读下去会让第一个面
之后的所有面错位。改成「读完一个元素就跳到下一行」之后，
同一模型的 ASCII 版和二进制版给出了逐项相同的结果（1000 个三角形、502 个顶点、
同样的体积和欧拉数）。

**质量属性**（对应它的 `mesh.center_mass` / `mesh.moment_inertia` /
`mesh.principal_inertia_components`）。算法和体积用的是同一套：把网格拆成
「原点 + 三角形」的四面体，用散度定理把体积分变成面积分——体积是零阶矩、
重心是一阶矩、转动惯量是二阶矩。

这项对 3D 打印有实际意义：**重心偏了的模型打印时容易倒**，而这不是几何上
「坏」，水密性检查一个字都不会说。

拿 `examples/box.3mf`（10×20×30 的长方体）验算，和闭式解逐位吻合：
重心正好是 (5, 10, 15)；三个主惯量是 650000 / 500000 / 250000，
对上 m(b²+c²)/12 = 6000×1300/12。

**有向包围盒**（对应它的 `mesh.bounding_box_oriented`）。轴对齐的包围盒对
斜着摆的模型会虚胖——一根斜放的棍子会被一个巨大的立方体包住。有向包围盒
用主成分分析找主轴，贴着模型包上去，能看出「它实际占多大地方」。

⚠️ 一个如实说明：PCA 求的不是**最小体积**包围盒（精确解要凸包 + 旋转卡壳）。
而且点云各向同性时主轴不唯一——立方体就是典型，任何一组正交基都同样「正确」，
硬算出来的方向完全由浮点噪声决定。这种情况实现里退回轴对齐，至少保证盒子是紧的。

**射线求交**（对应它的 `mesh.ray` 和 `mesh.nearest`）。用 Möller–Trumbore
算法，不预先算平面方程，每一步都能提前退出。

### 顺带多做了这一项：壁厚检查（trimesh 没有）

有了射线求交之后，往上搭了一层 trimesh 没做的事：

**从表面往模型里面打一条射线，打到对面有多远，那里的壁就有多厚。**
在模型上撒一千个采样点，取最小值。壁太薄是 3D 打印最常见的失败原因之一——
薄壁打不出来或者一碰就碎，而**从外面看模型完完整整，看不出哪里薄**。

验算：10×20×30 的长方体量出 10（最短那条边）；空心盒子（外壳 10³、
内腔 6³）量出 **2**，正好是壁厚；圆环量出 3.95，正好是管子直径。

两点如实说明：

- 采样是**确定性**的（低差异序列不是随机数）——「修复前 0.42、修复后 0.38」
  这种对比才有意义
- 采一千个点是 O(n·1000)，逐面打是 O(n²) 跑不动。代价是**可能漏掉个别极薄处**，
  所以报出来的是「**最薄处不超过** X」，不是「处处都大于 X」

**单位换算和装配体**：3MF 里写 `inch` 而不换算，体积会差 25.4³ ≈ 16387 倍；
对象可以由别的对象拼出来，各带一个 4×3 的行向量变换矩阵。

**拓扑判据本身**也是同一套：水密性 = 每条边恰好被两个面共用，
绕向一致性 = 相邻两面必须以相反方向使用共用边。

---

## 五、能力边界

这些能力 trimesh 有、stlkit 没有：

- 布尔运算（交 / 并 / 差）
- 体素化、拉普拉斯平滑、细分
- 包围球 / 包围柱
- 导入 OFF / GLTF / XAML / 3DXML
- 导出 GLB / COLLADA / DXF / SVG
- 交互式 3D 预览窗口（stlkit 只出静态 SVG 等轴测图）

它们服务的是「对网格做变换和运算」，stlkit 服务的是「判断这个网格能不能打印」。
两条线要回答的问题不同，功能的取舍自然也不同。

---

## 六、MoonBit 生态内部的对比

MoonBit 生态里的 3D 相关包可以分几类。**逐类说清楚和 stlkit 的关系**——
检索过 mooncakes 全部 2439 个包，把 3D 相关的候选逐个过了一遍。

### 1. 渲染引擎和运行时

`three`（three.js 的类型化 FFI 绑定）、`luna_three`（声明式 three.js）、
`kagura_engine` / `kagura_core` / `kagura_physics` / `kagura_ui`（渲染引擎）、
`crater-renderer`、`moon-ray`（Monte Carlo 路径追踪）、`raylib` 的几个绑定、
`cube_render`、`vibecraft`、`renderer2d`、`sfengine`。

- **它们**：把模型画出来。`moon-ray` 甚至带 BVH 加速和 PBR 材质
- **stlkit**：不画。出的是体检报告，不是图像

### 2. 渲染用的网格 / 几何原语

`mesh3d`（网格和顶点格式原语，从 kagura 抽出）、`geom`（Vec2 / Vec3 / Mat4 /
Quaternion）、`anim3d`（变换层级、关键帧动画、骨骼绑定）、`geometry3d`、`kagura_core`。

`mesh3d` 是这一类里最相关的一个，逐条说：

- **它**：渲染管线的网格数据结构。OBJ 解析器**逐面展开顶点**，源码注释原文就是
  *Emit vertices for this face*——没有共享顶点表
- **stlkit**：保留三角形与顶点的对应关系

这一条决定能不能回答「每条边被几个面共用」，而那正是判断能不能打印的**唯一依据**。
另外它的 `Mesh3D::from_obj` 返回 `Mesh3D` 而不是 `Result`，顶点索引越界静默按
`0.0` 处理；stlkit 返回 `Result`，错误带行号或字节偏移。

### 3. 渲染资产品管（glTF）—— 生态里最接近的一个

`moonbit-gltf-tools` 值得单独说，因为它的**形态**和 stlkit 很像：读、校验、处理
3D 资产，有 `quality_gate`、`mesh_cost_report`、给 CI 用的机器可读报告。

但它的「质量」是**渲染口径**的。查过源码：

- `g_quality.mbt` 里 `quality_gate` 检查的是纹理数量、动画通道数、骨骼节点数、
  double-sided 材质数、draw call 预算
- `g_mesh_analysis.mbt` 里 `mesh_cost_report` 报的是顶点字节数、属性槽位、
  顶点数最多的 mesh、没索引的 mesh 数

**这两个文件里 `watertight` / `manifold` / `boundary` / `degenerate` 出现 0 次。**
它不查水密性、非流形边、退化面——它管的是「这个资产渲染起来贵不贵」，
不是「这个网格能不能打出来」。

而且它做的是 **glTF/GLB**（渲染资产格式，带场景图、材质、动画），
stlkit 做的是 **STL / OBJ / 3MF / PLY**（3D 打印和三维扫描格式）。

### 4. 网格优化（渲染性能）

`meshopt_mbt`（MeshOptimizer 的绑定）：量化、顶点重映射、索引/顶点编解码、
顶点缓存 / overdraw 优化、简化、meshlet、stripification、分析、切线生成、空间排序。

- **它**：把网格改成**渲染更快**的样子。「分析」也是这个口径——顶点缓存命中率、
  overdraw。而且是 **native 绑定**，不是纯 MoonBit
- **stlkit**：不改网格，判断它能不能打印

### 5. 计算几何（偏 2D）

`computational-geometry`、`loop_invariants_geometry`、`generative_patterns`。
做的是凸包、交点、噪声场这类算法，基本是 2D 的。

### 6. 计算机视觉 / 相机几何

`moonbit_camera_models`（相机模型与 3D 坐标变换）、`moon-cv-geometry`
（多视图几何、单应、对极约束、RANSAC）、`fiducial_marker`（ArUco/AprilTag
检测与 3D 位姿估计）、`moonbit-feature-forge`。

- **它们**：处理**图像里的 3D**——从照片重建点云、算位姿
- **stlkit**：处理**已有的网格文件**

### 7. 其它领域的几何工具

- `moon-shapefile` / `moonbit9`：GIS 矢量数据（Shapefile）的读写
- `moonbit-gerberkit`：PCB 的 Gerber / Excellon 解析 + DFM 检查

`moonbit-gerberkit` 的**形式和 stlkit 几乎一样**（解析 + 几何 + 制造性检查），
但领域完全不同：PCB 是 2D 的。

### 8. 结论

把上面几类合起来看：**MoonBit 生态里没有任何一个包做三角网格质量验证**——
水密性、非流形边、退化面、绕向一致性，一个都没有。

最接近的两个都差一层：`moonbit-gltf-tools` 管的是**渲染成本**，
`mesh3d` 连拓扑都不保留。

## 七、一句话总结

| | |
| --- | --- |
| trimesh | Python 生态的通用三角网格库，需要 Python + numpy，提供网格操作的完整工具集 |
| 其它 MoonBit 3D 包 | 渲染方向，输出给 GPU 的几何，不做质量验证 |
| **stlkit** | 回答「这个模型能不能 3D 打印」。纯 MoonBit，无 FFI，三端可跑；库 + 命令行 + 网页三种形态；能流式扫描几百 MB 的扫描件 |

**它们回答「模型长什么样」，stlkit 回答「模型能不能打印」。**
