3.2 从 STEP 到零件 STL:cascadio 自动化导出

上下文回顾 :3.1 我们理清了
STEP → GLB → STL → MJCF的数据流。本节聚焦前半段------如何把ElectronBot.step里的几百个实体,自动拆成一个个独立的 STL 网格。
一个重要澄清 :本节文件名沿用了老教程的「FreeCAD 命令行导出」,但当前仓库的实际做法并不依赖 FreeCAD 。项目已改用cascadio + trimesh的纯 Python 流水线(scripts/step_to_mjcf.py),一条命令跑完,无需打开 FreeCAD。assets/cad/里的.FCStd/.FCMacro是早期人工建模阶段的产物,仅作结构参考。
1. 为什么放弃手工 FreeCAD 导出?
✗ 手工 FreeCAD 导出 300+ 零件逐个手工导出 坐标 / 命名难对齐脚本 换图纸就要重来(不可复现) ✓ 全自动 cascadio + trimesh STEP 进 → 零件 STL 出 中间零人工、可复现 一条命令跑完 图 3.2-1 手工 FreeCAD 导出 vs 全自动流水线:数量大、不可复现是根本矛盾。
早期方案是:在 FreeCAD 里打开工程 → 手动选中零件 → 一个个 Export STL。缺点很明显:
- 300+ 个零件手工导出不现实;
- 每个零件的坐标、命名难以和后续脚本对齐;
- 无法复现------换一版图纸就要重来一遍。
所以现在改成全自动:STEP 进,零件 STL 出,中间零人工。
2. 核心依赖:cascadio 与 trimesh
cascadio OpenCASCADE · STEP→GLB trimesh 读 GLB · 遍历场景图 numpy 坐标 / 变换矩阵 图 3.2-2 三个核心依赖各司其职:三角化、场景图遍历、矩阵计算。
| 库 | 作用 |
|---|---|
cascadio |
基于 OpenCASCADE 的 STEP → GLB 转换器,负责把 B-Rep 实体三角化 |
trimesh |
读取 GLB,遍历其场景图(scene graph),逐节点导出网格 |
numpy |
坐标 / 变换矩阵计算 |
安装(若未装):
bash
pip install cascadio trimesh numpy
3. 第一步:STEP → GLB
ElectronBot.step STEP 输入 cascadio.step_to_glb 三角化(仅首次) ElectronBot_scene.glb 缓存 + trimesh.load 产物 = 场景图:每个零件是一个带 4×4 变换矩阵的节点 图 3.2-3 STEP 经 cascadio 三角化为 GLB 并缓存,trimesh 加载后得到带节点层级的场景图。
step_to_mjcf.py 首先调用 cascadio 把整份 STEP 转成一个 GLB,并缓存下来,避免每次重跑都做一次耗时的三角化:
python
import cascadio, trimesh
glb_path = "assets/meshes/ElectronBot_scene.glb"
if not os.path.exists(glb_path):
cascadio.step_to_glb(step_path, glb_path) # 仅首次执行
scene = trimesh.load(glb_path) # 得到一个 trimesh.Scene
得到的 scene 是一个场景图:每个零件是图里的一个节点,节点上挂着该零件的三角网格,以及它相对父节点的 4×4 变换矩阵。这层「节点 + 变换」信息,正是后面重建关节树和摆正零件位置的关键。
4. 第二步:遍历场景图,逐件导出 STL
scene.graph 遍历 每个几何节点 apply_transform 世界坐标网格 平移到质心原点 -centroid step_*.stl (世界) 校验对齐 step_*_local.stl MJCF 引用 图 3.2-4 遍历场景图 → 每个零件导出两份 STL(世界坐标用于校验、本地坐标供 MJCF 引用)。
脚本遍历 scene.graph 里的每个几何节点,把它作为独立零件导出:
python
for node_name in scene.graph.nodes_geometry:
transform, geom_name = scene.graph[node_name] # 该节点的世界变换 + 几何名
mesh = scene.geometry[geom_name].copy()
mesh.apply_transform(transform) # 应用世界变换 → 世界坐标网格
# 1) 世界坐标 STL(用于校验对齐)
mesh.export(f"assets/meshes/step_parts/step_{safe_name}.stl")
# 2) 记录质心,做坐标本地化(下一节详解)
centroid = mesh.centroid
mesh.apply_translation(-centroid) # 平移到原点
mesh.export(f"assets/meshes/step_parts/step_{safe_name}_local.stl")
每个零件因此产生两个 STL:
step_<name>.stl:保留在世界坐标里,用来肉眼核对整机装配对不对;step_<name>_local.stl:把网格平移到自身质心原点,MJCF 里<mesh>引用的是这一批,零件的实际位置改由<body pos="...">承载。
5. 导出结果
step_parts/ step_*.stl × 369(世界坐标) step_*local.stl × 349 (本地坐标,MJCF 引用) 节点名形如 Open_CASCADE_STEP_translator... → 清洗后作文件名 / mesh name 图 3.2-5 导出目录结构与节点名清洗规则。
跑完后 assets/meshes/step_parts/ 里大约有:
step_parts/
├── step_xxx.stl × 369 (世界坐标)
├── step_xxx_local.stl × 349 (本地坐标,MJCF 引用)
└── ...
STEP 里由 Open CASCADE 生成的节点名往往形如
Open_CASCADE_STEP_translator_6_8_54_1_1_,脚本会做字符清洗(非法字符替换成_)后作为文件名和<mesh name>。所以你在 XML 里会看到一堆这种长名字,属正常现象。
6. 本节小结
STEP cascadio GLB 缓存 trimesh 两份 STL / 零件 世界(校验) + 本地(MJCF) 图 3.2-6 本节收束:cascadio 三角化 + trimesh 遍历,每个零件导出两份 STL。
- 实际流水线用 cascadio(STEP→GLB)+ trimesh(GLB→逐件 STL),不是 FreeCAD 命令行。
- GLB 会被缓存到
assets/meshes/ElectronBot_scene.glb,二次运行秒开。 - 每个零件导出两份 STL:世界坐标(校验用)+ 本地坐标(
_local,MJCF 引用)。 - 坐标本地化的动机是「网格归零、位置交给 body」,具体在 3.4 节展开。
本节涉及的资源文件
scripts/step_to_mjcf.py(export_parts/ 场景图遍历部分)assets/cad/ElectronBot.step(输入)assets/meshes/ElectronBot_scene.glb(GLB 缓存)assets/meshes/step_parts/*.stl(输出)
⬅️ 上一节:3.1 三类文件的定位 | ➡️ 下一节:3.3 Mesh 方案选择:外部引用 vs Inline