CLI-Anything 3MF 网格几何编辑器:圆柱孔检测、径向缩放与切片元数据无损保留实战
cli-anything-3mf 是 CLI-Anything 项目中面向 3D 打印 3MF 文件的"功能感知"网格几何编辑器:它能通过多平面截面分析自动检测模型中的圆柱孔、测量孔径,并沿径向缩放孔壁顶点实现精确改径,同时保留 BambuStudio / PrusaSlicer 的切片元数据。读完本文,你将掌握该工具的 5 个核心命令(info / inspect / resize / repair / compare)的完整用法与全部可选参数,理解其孔检测置信度计算、径向缩放公式与网格修复流水线的源码级原理,并知道它如何做到重打包后切片软件"零配置丢失"。
1. 工具定位:不止于"三角面汤"
传统 3D 打印 CLI 工具(PrusaSlicer CLI、BambuStudio CLI、admesh)把网格当作不透明的三角面集合处理,而 cli-anything-3mf 提供的是 feature-aware geometry editing(功能感知的几何编辑)——它可以"看懂"网格中的圆柱孔这一特征,测量其直径,并通过命令行修改它。
安装方式(继承自仓库技能文档 SKILL.md 与 README):
pip install git+https://github.com/HKUDS/CLI-Anything.git#subdirectory=3MF/agent-harness
开发模式安装:
cd 3MF/agent-harness
pip install -e ".[dev]"
根据 setup.py,依赖为 click>=8.0.0、prompt-toolkit>=3.0.0、numpy>=1.24.0、scipy>=1.10.0、trimesh>=4.0.0,控制台入口注册为 cli-anything-3mf=cli_anything.threemf.threemf_cli:main。
2. 3MF 文件格式基础
理解该工具的前提是理解 3MF 容器结构。一个 3MF 文件本质是 ZIP 归档,内部包含(见 3MF.md):
[Content_Types].xml— MIME 类型声明;_rels/.rels— 关系定义(OPC 标准);3D/3dmodel.model— XML 网格数据,核心命名空间为http://schemas.microsoft.com/3dmanufacturing/core/2015/02;Metadata/— 切片器设置、缩略图、plate 配置(BambuStudio、PrusaSlicer 等)。
parser.py 的解析策略是"网格数据结构化 + 其余内容字节级保留":parse_3mf() 打开 ZIP 后,除主模型 XML 外的所有成员都被读入 raw_entries 字典({zip_path: bytes}),模型 XML 则解析为不可变的 MeshData(N×3 float64 顶点数组 + M×3 int32 三角形索引数组)与 ThreeMFData(meshes、unit、model_path、metadata、raw_entries)两个 frozen dataclass。这一设计是后面"无损重打包"的基础。
3. 五个核心命令
CLI 入口在 threemf_cli.py,基于 Click 实现,直接运行 cli-anything-3mf(不带子命令)会进入交互式 REPL。
3.1 info:网格统计
cli-anything-3mf info <file.3mf>
输出每个 mesh 对象的顶点数、面数、包围盒(min/max/size)、水密性(watertight)、体积与表面积。源码中 info 命令对每个 mesh 调用 threemf_backend.py 的 compute_mesh_stats():体积仅在网格水密时给出,否则显示 N/A (not watertight)。
3.2 inspect:圆柱孔检测
cli-anything-3mf inspect <file.3mf>
每个检测到的孔输出:直径、中心坐标、轴向范围(axis_min..axis_max)、置信度、孔壁顶点数。
该命令的完整参数集(来自 threemf_cli.py#L154-L158):
| 参数 | 短写 | 默认值 | 说明 |
|---|---|---|---|
--planes |
-n |
20 | 垂直于孔轴的截面平面数量 |
--min-diameter |
— | 0.5 | 最小孔直径(mm),过滤掉微小特征 |
--min-confidence |
— | 0.7 | 最小检测置信度(0~1) |
--axis |
-a |
0 | 孔轴方向:0=X,1=Y,2=Z(截面垂直于该轴) |
--mesh |
-m |
0 | 多对象 3MF 中要分析的 mesh 索引 |
注意 --axis 默认指向 X 轴,源码注释解释原因是"大多数 3D 打印件的孔沿 X 方向"。检测参数由 inspector.InspectParams dataclass 承载,上述默认值与 inspector.py#L32-L39 中的定义一致。
3.3 resize:孔径缩放
# 将孔 0 和孔 1 改径到 4.2mm
cli-anything-3mf resize <file.3mf> --hole 0 --hole 1 --diameter 4.2 -o output.3mf
# 一次改多个孔(短写形式)
cli-anything-3mf resize model.3mf -h 0 -h 1 -h 2 -h 3 -d 4.2 -o output.3mf
resize 的参数在 inspect 参数基础上增加:
| 参数 | 短写 | 必填 | 说明 |
|---|---|---|---|
--hole |
-h |
是(可重复) | 要改径的孔 ID,来自 inspect 输出 |
--diameter |
-d |
是 | 目标直径(mm),必须为正数 |
--output |
-o |
是 | 输出文件路径 |
--overwrite |
— | 否 | 输出已存在时允许覆盖,否则报 FileExistsError |
关键约束:resize 内部会重新执行一次孔检测以把 hole_id 解析回几何,因此 hole_id 编号只对同一次检测有效——你必须传与 inspect 时完全相同的检测参数(尤其是 --axis),否则 ID 对不上。这一点在 modifier.py#L39-L46 的文档字符串中被明确强调。
resize 完成后源码会自动执行一轮网格修复(见 §5.3),--json 模式下输出的 repairs 字段会报告合并了多少重复顶点、移除了多少个退化面;人类可读模式则打印 Auto-repair: N fixes applied。
3.4 repair:网格修复
cli-anything-3mf repair <file.3mf> -o repaired.3mf
修复内容:合并重复顶点、移除退化三角面、移除无引用顶点、修正法线朝向。报告字段为 vertices_merged / degenerate_faces_removed / unreferenced_vertices_removed / 最终顶点面数。
3.5 compare:两文件对比
cli-anything-3mf compare <file1.3mf> <file2.3mf>
输出顶点数、面数、体积(mm3)的 file1/file2/diff 三列对比,以及双方的水密状态。实现是 inspector.py 的 compare_meshes(),对两个文件各自调用 compute_mesh_stats() 后取差值。
3.6 交互式 REPL
直接运行 cli-anything-3mf 进入 REPL(源码中是隐藏的 repl 子命令)。REPL 提示样式由 repl_skin.py 统一提供;REPL 内输入用 shlex.split 切分后回灌给同一个 Click 命令树,因此支持全部子命令与选项,help 查看可用命令,quit/exit/q 退出。REPL 模式下的错误不会触发进程退出(_repl_mode 全局位控制)。
4. JSON 输出:面向 Agent 的机器可读接口
所有命令支持全局 --json 标志,这是该工具"agent-native"定位的核心:
cli-anything-3mf --json inspect model.3mf
从 threemf_cli.py#L35-L47 的 output() 函数看,--json 模式下所有数据以 json.dumps(data, indent=2) 输出;错误处理同样双通道:handle_error 装饰器捕获 RuntimeError/FileNotFoundError/ValueError/IndexError 后,JSON 模式输出 {"error": ..., "type": ...},文本模式向 stderr 打印 Error: ... 并以退出码 1 结束。这让 LLM Agent 可以稳定地解析成功/失败两种结果——例如先 --json inspect 拿到 hole_id 列表,再程序化拼装 resize 命令。
5. 核心算法解析(源码级)
5.1 孔检测:多平面截面 + Kasa 圆拟合 + 层次聚类
inspect_mesh()(inspector.py#L54-L190)的完整流程:
第一步,多平面截面。 cross_section_circles()(threemf_backend.py#L243-L351)沿指定轴取 num_planes 个等距截面,但截平面不是从边界开始,而是向内收缩 PLANE_INSET_FRACTION = 0.02(轴跨度的 2%),避免在端面处切出退化片段。每个截面用 trimesh 的 section() 得到 Path3D,再逐条离散轮廓投影到二维平面拟合圆;轮廓点少于 5 个的候选直接跳过。
第二步,Kasa 代数圆拟合。 fit_circle_least_squares() 最小化代数距离:解超定方程组 x² + y² + a·x + b·y + c = 0,中心为 (-a/2, -b/2),半径为 sqrt(a²/4 + b²/4 - c),并返回 RMS 拟合误差。相对拟合误差超过半径 5% 的轮廓判定为"不够圆"而丢弃。
第三步,层次聚类分组。 不同截面上的圆属于同一个孔,当且仅当它们的中心和半径都接近。inspector.py#L271-L297 的 _group_circles() 用 (center_x, center_y, radius) 三元特征做 scipy 单链接层次聚类,距离阈值 _GROUP_DISTANCE_MM = 0.5mm。半径参与聚类是为了区分同心特征——例如实体上打穿的孔,截面会同时得到内孔和外轮廓两个圆,若只按中心聚类,二者半径会被平均成一个"假圆"。
第四步,置信度过滤。 置信度公式(inspector.py#L122-L130):
confidence = (detected_planes / total_planes) × (1 − mean_fit_error / mean_radius)
即"检测一致性 × 拟合质量",低于 min_confidence(默认 0.7)或平均直径小于 min_hole_diameter(默认 0.5mm)的候选被丢弃。
第五步,内孔判别。 这是区分"孔"与"外表面圆柱"的关键:孔的圆柱壁面向内(法线指向轴线),而外边界、实心圆柱体、凸台的法线朝外。_is_interior_hole() 采样孔壁面上的法线,计算其径向分量均值,小于 0(指向轴线)才认定为孔;若网格绕向不一致(winding inconsistent)则保守地保留候选。注释说明该判别同时兼容通孔与盲孔,而基于顶点包围的测试会在盲孔底部失效。
此外,轴向范围不直接取截平面层级,而是用 _wall_axial_extent() 从实际孔壁顶点反推(带一个 inset 的外扩搜索窗),以恢复恰好位于零件端面上的 rim 顶点——这正是让后续 resize 能移动孔缘顶点的修复点。
5.2 孔径缩放:径向顶点缩放
resize_single_hole()(modifier.py#L92-L146)对每个目标孔:
- 计算每个顶点到孔轴的距离,选出径向距离落在
[radius − 0.06, radius + 0.06]mm 内且轴向坐标落在孔范围内(同样带 0.06mm 容差)的孔壁顶点,容差常量_WALL_RADIUS_TOLERANCE = 0.06; - 对这些顶点执行径向缩放:
new_pos = center + direction × (new_r / old_r)
即只修改垂直于孔轴的两个坐标,轴向坐标不动,顶点的角向位置(angular position)与网格拓扑(三角形连接关系)完全保留;
3. 返回新的 MeshData(不可变 dataclass 的 replace 副本)与改动报告 {hole_id, old_diameter, new_diameter, vertices_moved}。
对未知 hole_id,resize_holes() 会抛出包含已检测 ID 列表的 ValueError,便于 Agent 自纠错。
5.3 网格修复流水线
repair.py 的 repair_mesh() 分四步:
- 合并重复顶点:
merge_duplicate_vertices()先把坐标四舍五入到 6 位小数,再用 numpy 结构化视图 +np.unique做快速去重,并同步重映射三角形索引; - 移除退化面:任何两个顶点索引相同的三角形(合并后自然产生)被
_nondegenerate_face_mask()筛掉,且同步过滤对应的triangle_attributes; - 移除无引用顶点并压缩索引;
- 另有独立的
fix_normals()通过trimesh.repair.fix_normals()修正绕向、使法线一致朝外。
resize 命令自动复用该流水线,因为径向缩放可能让原本不相邻的顶点重合、产生退化面。
6. 切片器兼容性与无损重打包
这是该工具区别于普通网格编辑器的核心承诺:输出文件可以在原切片软件中重新打开且不丢配置。实现机制:
字节级保留非网格内容。 parse_3mf() 把所有非模型 ZIP 成员(缩略图、Bambu 的 project_settings.config/model_settings.config、PrusaSlicer 的 slic3r_pe 配置、层厚曲线等)存入 raw_entries;write_3mf() 重写 ZIP 时原样写回,只替换模型 XML。
XML 层保留非网格元素。 重打包时 _rebuild_model_xml()(parser.py#L298-L360)重新读取源文件 XML,仅把 id 匹配的 <object> 的 <mesh> 子树换成修改后的顶点/三角形数组,其余元素(metadata、build item、扩展命名空间、切片器嵌入的属性)原样保留;命名空间前缀通过 ET.register_namespace 注册(核心命名空间与 Bambu 的 b 前缀),避免序列化出 ns0 之类的陌生前缀。
三角形属性保留。 解析时 <triangle> 元素上除 v1/v2/v3 外的所有属性(3MF 材料属性 pid、p1、p2、p3 及未知 vendor 属性)被存入 MeshData.triangle_attributes 并随行保存;写回时逐三角形恢复。这意味着某三角形在编辑中存活,它的材料属性就跟着存活;退化面被修复移除时属性同步过滤。
明确的边界(能力限制)。 纯 component 对象(只有 <components> 没有 <mesh>)和 component/build 的 transform 属性作为未触碰的 XML 保留,但当前几何操作不会把 component transform 解析进网格坐标,也无法直接编辑组件实例的 transform。此外,孔检测假设网格绕向一致(有效 3MF 文件满足此条件);--axis 参数意味着一次检测只覆盖一个轴方向的孔,其他方向的孔需要换 --axis 重新 inspect。
7. 测试体系
tests/test_core.py(约 1260 行)覆盖 parser、backend、inspector、repair、modifier 全部五个模块,全部基于合成 numpy 数据与内存生成的 3MF 夹具,不依赖外部文件。例如三角形属性保留测试用内嵌 XML 构造含 pid/p1/p2/p3 与 vendor:tag 属性的 3MF 夹具,并包含 component-only 对象与 build transform 来验证无损保留;README 提到另有 30+ E2E 测试。
8. 适用前提与使用建议
- 输入必须是符合 3MF Core Specification 的文件,几何单位以文件内
unit属性为准(默认millimeter),命令中的 mm 参数与之对应; - 典型工作流:先
inspect确认 hole_id 与直径 → 用相同检测参数resize→compare验证前后差异 → 用切片软件重开确认配置未丢; - 从源码结构看,孔检测只识别"圆柱形"孔,锥形/异形孔不会以稳定置信度被识别;
--planes增加可提高对短孔/浅盲孔的检测稳定性,但会增加截面开销; - 修改后的文件如需再编辑,请以最近一次输出为输入并重新 inspect——hole_id 不跨文件持久。
本文内容基于仓库 3MF/ 目录下的技能文档、SOP 文档与 cli_anything/threemf 源码整理,引用的默认值、容差常量(0.06mm 壁面容差、0.5mm 聚类阈值、2% 截面内缩、5% 圆拟合误差上限)均可在对应源码文件中逐行核对。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00