1. 从一句话到三维实体:text-to-cad 到底在解决什么问题
把"画一个 M8 法兰螺母"这句话直接变成可加工的 STEP 文件,这件事在几年前还只存在于论文里。现在 text-to-cad 这类工具已经能跑通从自然语言到 B-rep 实体的完整链路,虽然离"说完就出图"还有距离,但作为辅助建模手段已经相当能打。我最近花了两周时间把这条链路从头到尾摸了一遍,踩了不少坑,也总结出一些文档里不会写的经验,这里一次性讲清楚。
先说清楚 text-to-cad 是什么。它的核心思路是:用户输入一段自然语言描述(比如"一个外径 40mm、内径 20mm、厚度 5mm 的垫圈"),系统通过大语言模型解析出几何参数和拓扑关系,再调用 CAD 内核(常见的是 OpenCASCADE 或 CadQuery)生成三维实体,最后导出成 STEP、STL 或 URDF 等格式。它解决的核心痛点是:传统 CAD 建模需要人工在 GUI 里一步步拉伸、倒角、打孔,重复性极高;而 text-to-cad 把"描述"直接映射成"几何",特别适合参数化零件、批量变体、以及需要和代码流水线集成的场景。
适合谁来用?三类人最受益。第一类是机械工程师,手头有大量规格化零件(螺栓、垫圈、型材、法兰),用脚本生成比手动画快十倍。第二类是机器人方向的开发者,需要批量生成 URDF 里的连杆几何体,text-to-cad 可以直接输出 URDF 兼容的 mesh 和惯性参数。第三类是做仿真和 3D 打印的人,需要快速把想法变成可切片或可导入仿真器的模型。如果你是完全零基础的 CAD 小白,这篇文章也能看懂,但建议先补一下基本的几何概念(什么是 B-rep、什么是网格、STEP 和 STL 的区别),否则后面会有点吃力。
关键词里出现了 STEP、URDF、G-code,这三个正好对应了 text-to-cad 的三条主要输出路径:STEP 是精确 B-rep 交换格式,给传统 CAD 和 CAM 用;URDF 是机器人描述格式,给仿真器用;G-code 是数控加工指令,给机床用。理解这三者的区别和转换关系,是用好 text-to-cad 的关键。下面我会按"环境搭建 → 核心原理 → 实操步骤 → 踩坑排查 → 进阶玩法"的顺序展开,每一段都尽量给出可复现的命令和参数。
2. 环境搭建:为什么我最终选了 CadQuery 而不是直接调 FreeCAD
2.1 三种技术路线的取舍逻辑
text-to-cad 的底层实现大致有三条路:一是直接调用 FreeCAD 的 Python API,二是用 CadQuery 这种基于 OpenCASCADE 的脚本化建模库,三是自己封装 OpenCASCADE 的 C++ 接口。我三条都试过,最后稳定用的是 CadQuery,原因如下。
FreeCAD 的 API 功能全,但它的 Python 绑定在不同版本之间变动很大,0.19 和 0.21 的 Part 模块接口就有差异,而且 FreeCAD 启动慢、依赖重,在服务器上跑批量任务时经常因为 GUI 相关的库缺失而报错。自己封装 OpenCASCADE 最灵活,但开发成本太高,光是编译和链接就能耗掉一整天,不适合快速验证。CadQuery 的好处是:纯 Python、依赖干净、API 稳定、文档齐全,而且它本身就是为"代码化建模"设计的,和 text-to-cad 的"描述转几何"思路天然契合。
提示:CadQuery 底层也是 OpenCASCADE,所以它生成的 STEP 文件精度和 FreeCAD 一致,不存在"精度不够"的问题。区别只在 API 封装层。
2.2 实际安装步骤与依赖版本锁定
安装 CadQuery 最省事的方式是用 conda,因为它的依赖里有几个 C 库(比如 OCCT、VTK)用 pip 装容易出问题。我实测下来最稳的组合是 Python 3.10 + conda-forge 渠道。
conda create -n t2cad python=3.10 conda activate t2cad conda install -c conda-forge cadquery=2.4装完之后验证一下:
import cadquery as cq result = cq.Workplane("XY").box(10, 10, 10) cq.exporters.export(result, "test.step") print("OK")如果这一步能生成 test.step,说明环境没问题。这里有个坑:如果你之前装过 FreeCAD 的 pip 包,它可能会和 CadQuery 抢 OCCT 的动态库,导致ImportError: DLL load failed。解决办法是建一个干净的虚拟环境,别在系统 Python 里混装。
2.3 大语言模型接口的准备
text-to-cad 的"text"部分需要一个 LLM 来解析自然语言。我用的是本地部署的开源模型加一个规则兜底层,原因是:纯靠 LLM 解析几何参数不稳定,同一个描述两次可能给出不同的数值。我的做法是让 LLM 只负责"抽取结构化参数",比如把"外径 40 内径 20 厚 5 的垫圈"抽成{"type": "washer", "od": 40, "id": 20, "thickness": 5},然后由确定性的 Python 函数根据这个字典生成几何。这样既保留了自然语言的灵活性,又保证了数值的确定性。
import json def parse_description(text): # 实际项目中这里调用 LLM,返回 JSON 字符串 # 这里用规则模拟 prompt = f"从下面描述中抽取几何参数,只返回JSON:{text}" # response = llm_client.chat(prompt) response = '{"type": "washer", "od": 40, "id": 20, "thickness": 5}' return json.loads(response)这个"LLM 抽参数 + 代码建几何"的分层设计是我踩了坑之后才改的。一开始我让 LLM 直接生成 CadQuery 代码,结果它经常写出语法正确但几何错误的代码,比如把circle(20)和circle(40)的顺序搞反,导致内径比外径还大。分层之后,几何正确性由代码保证,LLM 只负责它擅长的语义理解。
3. 核心原理:自然语言是怎么一步步变成 STEP 文件的
3.1 从语义到参数:LLM 的职责边界
很多人以为 text-to-cad 就是"让 AI 写 CAD 代码",其实更准确的说法是"让 AI 做语义到参数的映射"。几何内核(OpenCASCADE)本身是确定性的,给它同样的参数永远得到同样的实体。所以整个系统的可靠性瓶颈不在几何计算,而在语义解析。
我总结的职责划分是这样的:LLM 负责识别零件类型(是垫圈还是法兰还是支架)、抽取尺寸数值、理解单位(mm 还是 inch)、识别特征(有没有倒角、有没有螺纹孔)。而几何内核负责把这些参数变成实际的 B-rep 实体。中间用一个严格的 schema 做约束,比如垫圈必须有 od、id、thickness 三个正数,法兰必须有 bolt_circle_diameter 和 bolt_hole_count。
注意:单位识别是最容易出错的地方。中文描述里"40"默认是毫米,但英文描述里"40"可能是英寸。我的做法是在 prompt 里强制要求 LLM 输出单位字段,如果没识别到就默认 mm 并在日志里打警告。
3.2 B-rep 与网格的本质区别
STEP 文件里存的是 B-rep(边界表示),它用数学曲面(平面、圆柱面、NURBS 曲面)精确描述几何,一个圆柱面就是一个方程,不是一堆三角形。而 STL 存的是三角网格,圆柱面被离散成很多小三角形。这个区别决定了:STEP 可以无损地做布尔运算、倒角、抽壳,而 STL 做这些操作会累积误差。
text-to-cad 生成 STEP 的意义在于:你可以在 CAD 软件里继续编辑它,可以导入 CAM 软件生成刀路,可以用于精确的装配干涉检查。而如果你只需要 3D 打印或做视觉仿真,STL 就够了。我的建议是:中间产物一律用 STEP,最后按需转 STL 或 URDF。
# 生成 STEP 后转 STL import cadquery as cq result = cq.Workplane("XY").circle(20).extrude(5).faces(">Z").workplane().hole(10) cq.exporters.export(result, "washer.step") cq.exporters.export(result, "washer.stl", tolerance=0.01)这里的tolerance=0.01控制网格精度,数值越小三角形越多、文件越大。做 3D 打印用 0.01 够了,做碰撞检测建议 0.005。
3.3 URDF 导出时那些没人告诉你的细节
URDF 是机器人描述格式,它需要每个连杆有 visual 和 collision 两套几何,还要有 inertial 参数(质量、质心、惯性张量)。text-to-cad 生成的几何体要导入 URDF,有几个坑必须提前知道。
第一,URDF 的 visual 和 collision 通常用不同的 mesh:visual 用高精度 STL 好看,collision 用简化后的凸包或低精度 mesh 算得快。如果你直接把高精度 STL 同时用于两者,仿真会慢到怀疑人生。第二,惯性参数不能随便填,质量要根据材料密度和体积算,惯性张量要用平行轴定理从质心算起。第三,URDF 里的 mesh 路径是相对路径,导入 CoppeliaSim 时如果路径不对会显示不出来,建议用package://前缀或者绝对路径。
# 计算简单圆柱体的惯性参数 import numpy as np def cylinder_inertia(mass, radius, height): Ixx = mass * (3*radius**2 + height**2) / 12 Izz = mass * radius**2 / 2 return {"ixx": Ixx, "iyy": Ixx, "izz": Izz, "ixy": 0, "ixz": 0, "iyz": 0}这个函数我用了很多次,比手算靠谱。注意 URDF 的惯性张量是相对于连杆坐标系原点的,如果你的几何体原点不在质心,还要做平移变换。
4. 实操全流程:从"画个垫圈"到导出可用的 STEP
4.1 第一步:把描述拆成可执行的参数结构
我拿"外径 40mm、内径 20mm、厚度 5mm 的垫圈"这个例子走一遍完整流程。首先定义参数 schema:
from pydantic import BaseModel, Field class WasherParams(BaseModel): od: float = Field(gt=0, description="外径 mm") id: float = Field(gt=0, description="内径 mm") thickness: float = Field(gt=0, description="厚度 mm") def validate_geometry(self): if self.id >= self.od: raise ValueError("内径必须小于外径") return True用 pydantic 做校验的好处是:LLM 输出的参数如果不符合几何约束(比如内径大于外径),会在这一层被拦住,不会传到几何内核里产生错误实体。这个校验层是我强烈建议加的,因为 LLM 偶尔会犯低级错误。
4.2 第二步:用 CadQuery 构建几何体
参数校验通过后,构建几何体:
import cadquery as cq def build_washer(params: WasherParams): result = ( cq.Workplane("XY") .circle(params.od / 2) .extrude(params.thickness) .faces(">Z") .workplane() .hole(params.id) ) return result washer = build_washer(WasherParams(od=40, id=20, thickness=5)) cq.exporters.export(washer, "washer.step")这段代码的逻辑是:在 XY 平面上画一个半径 20 的圆,拉伸 5mm 成圆柱,然后在顶面打一个直径 20 的孔。faces(">Z")选中 Z 方向最高的面,.workplane()在该面上建立工作平面,.hole(20)打孔。注意.hole()的参数是直径不是半径,这个和.circle()不一样,我第一次用的时候搞混了,结果孔比预期大一倍。
4.3 第三步:验证几何正确性
生成之后不能直接就用,要验证。我的验证清单有三项:体积对不对、包围盒对不对、STEP 能不能被重新导入。
import cadquery as cq # 重新导入验证 imported = cq.importers.importStep("washer.step") bb = imported.val().BoundingBox() print(f"包围盒: {bb.xlen} x {bb.ylen} x {bb.zlen}") # 期望: 40 x 40 x 5 vol = imported.val().Volume() expected = 3.14159 * (20**2 - 10**2) * 5 print(f"体积: {vol:.2f}, 期望: {expected:.2f}")体积验证特别有用,因为如果孔没打穿或者布尔运算失败,体积会明显偏大。我遇到过.hole()因为工作平面选错而没打穿的情况,体积一算就露馅了。
4.4 第四步:批量生成与命名规范
单个零件跑通后,批量生成就是循环的事。但命名规范要提前定好,否则文件一多就乱。我的命名规则是{类型}_{关键尺寸}_{版本}.step,比如washer_od40_id20_t5_v1.step。这样在文件管理器里排序和搜索都方便。
import os specs = [ WasherParams(od=40, id=20, thickness=5), WasherParams(od=30, id=15, thickness=3), WasherParams(od=50, id=25, thickness=6), ] os.makedirs("output", exist_ok=True) for p in specs: w = build_washer(p) name = f"washer_od{int(p.od)}_id{int(p.id)}_t{int(p.thickness)}_v1.step" cq.exporters.export(w, os.path.join("output", name)) print(f"生成: {name}")批量生成时要注意内存,CadQuery 的实体对象不会自动释放,如果一次生成几百个,内存会涨。我的做法是每生成 50 个就del一次并手动触发垃圾回收,或者干脆用多进程分批跑。
5. 踩坑实录:那些让我加班到凌晨的报错
5.1 布尔运算失败:为什么两个实体就是切不掉
布尔运算是 CAD 里最容易出问题的地方。我遇到过一次:一个法兰盘上要打 6 个螺栓孔,前 5 个都成功,第 6 个死活切不掉,报错BRepAlgoAPI_Fuse failed。排查了半天,发现是第 6 个孔的位置正好和法兰外边缘相切,导致布尔运算的容差判断失败。
解决办法有两个:一是把孔的位置往内移 0.1mm,避开相切;二是调大 OpenCASCADE 的模糊容差。我选了第一个,因为改容差可能引入其他问题。这个坑的教训是:几何体之间不要留相切关系,要么相交要么分离,相切是数值计算的噩梦。
提示:如果你做的是参数化零件,在参数校验层就检查"孔边缘到外边缘的距离必须大于 0.5mm",能提前拦住这类问题。
5.2 STEP 导入 CoppeliaSim 后模型不见了
这个问题困扰了我一个下午。STEP 文件在 FreeCAD 里打开正常,但导入 CoppeliaSim 后场景树里有节点,视口里却什么都看不到。原因有三个可能:一是模型尺寸太大或太小,相机没对准;二是模型的原点不在几何中心,导入后跑到很远的地方;三是 CoppeliaSim 对 STEP 的支持有限,需要先转成 STL 或 OBJ。
我最后确认是第三个原因。CoppeliaSim 的 URDF 导入插件对 mesh 格式的支持顺序是:OBJ > STL > DAE,STEP 基本不支持。所以正确流程是:CadQuery 生成 STEP → 转 STL → 在 URDF 里引用 STL → 导入 CoppeliaSim。转的时候注意坐标系,CadQuery 默认 Z 轴向上,而 URDF 和很多机器人仿真器默认 Z 轴向上但 Y 轴向前,可能需要旋转。
# STEP 转 STL 并调整坐标系 import cadquery as cq model = cq.importers.importStep("flange.step") # 如果需要绕 X 轴旋转 -90 度 model = model.rotate((0,0,0), (1,0,0), -90) cq.exporters.export(model, "flange.stl", tolerance=0.005)5.3 单位混乱导致的"模型大了 25.4 倍"
这是最经典也最致命的坑。LLM 把"1 inch"解析成了数值 1,但没带单位,代码默认按 mm 处理,结果模型小了 25.4 倍。反过来,如果 STEP 文件本身是英寸单位,导入时没做转换,模型就会大 25.4 倍。
我的解决方案是在整个流水线里强制统一单位:所有内部计算一律用 mm,LLM 输出的参数必须带单位字段,转换在入口处一次性完成。STEP 导出时显式指定单位:
cq.exporters.export(model, "part.step", unit="MM")导入时也要检查:
imported = cq.importers.importStep("part.step") # 检查包围盒尺寸是否合理 bb = imported.val().BoundingBox() if bb.xlen > 10000: # 超过 10 米肯定不对 print("警告:尺寸异常,可能是单位问题")5.4 G-code 生成前的模型检查清单
如果你要把 text-to-cad 生成的模型送去 CNC 加工,生成 G-code 之前必须检查几件事。第一,模型必须是封闭的实体(watertight),不能有破面或非流形边。第二,最小特征尺寸要大于刀具半径,否则加工不出来。第三,如果有深孔,要考虑刀具的长径比。第四,STEP 转 STL 时的精度要足够,否则曲面会有明显棱角。
# 检查实体是否封闭 solid = imported.val() if not solid.isValid(): print("实体无效,需要修复") # 检查体积是否为正 if solid.Volume() <= 0: print("体积异常,可能是破面")我一般会用 FreeCAD 的Part CheckGeometry工具做最终检查,它能找出自相交、破面、微小边等问题。text-to-cad 生成的模型大部分是干净的,但布尔运算多了之后偶尔会出问题。
6. 进阶玩法:把 text-to-cad 接进自动化流水线
6.1 用配置文件驱动批量变体生成
单个零件生成只是起点,真正的价值在于批量变体。我现在的做法是维护一个 YAML 配置文件,里面列出所有需要生成的零件规格,然后一个脚本跑完。
parts: - type: washer od: 40 id: 20 thickness: 5 - type: flange od: 100 bolt_circle: 80 bolt_count: 6 bolt_hole_dia: 8 thickness: 10import yaml with open("parts.yaml") as f: config = yaml.safe_load(f) for part in config["parts"]: if part["type"] == "washer": p = WasherParams(**{k:v for k,v in part.items() if k != "type"}) model = build_washer(p) elif part["type"] == "flange": model = build_flange(part) cq.exporters.export(model, f"output/{part['type']}_{part.get('od')}.step")这个模式特别适合产品系列化设计的场景,改一个 YAML 文件就能重新生成整个系列。
6.2 和版本控制结合:让 CAD 模型也能 diff
STEP 是二进制格式,没法用 git diff 看变化。我的做法是同时保存生成模型的 Python 脚本和参数 JSON,脚本和 JSON 是文本,可以 diff。这样每次改参数,git 记录的是"od 从 40 改成 45",而不是一个二进制文件的变更。团队协作时这个习惯能省很多沟通成本。
6.3 从 CAD 到 G-code 的最后一公里
如果你需要 G-code,通常的路径是:STEP → CAM 软件(如 FreeCAD Path 工作台或 Fusion 360 CAM)→ G-code。text-to-cad 本身不生成 G-code,但它生成的干净 STEP 能让 CAM 环节顺利很多。我试过用 FreeCAD 的 Path 工作台做简单的 2.5D 铣削,流程是:导入 STEP → 建立 Job → 选刀具 → 生成刀路 → 后处理成 G-code。对于垫圈、法兰这类回转体,其实用车削更合适,但 FreeCAD 的车削支持一般,复杂零件还是得上专业 CAM。
注意:G-code 和具体机床的后处理器强相关,同一份刀路给不同的机床要换后处理器。别指望一份 G-code 通吃所有设备。
6.4 一些让效率翻倍的小技巧
第一个技巧:把常用的零件模板封装成函数库,比如build_flange、build_bracket、build_shaft,每次用的时候只传参数。我现在的模板库覆盖了 80% 的常用零件,新零件基本是拼装已有模板。
第二个技巧:用 Jupyter Notebook 做交互式调试。CadQuery 在 Notebook 里可以直接显示 3D 模型(需要装jupyter-cadquery),改一个参数立刻看到结果,比反复导出 STEP 再打开 CAD 软件快得多。
第三个技巧:给每个生成的模型自动生成一张缩略图。用cadquery的exporters.export配合vtk渲染,或者简单点用 FreeCAD 的命令行模式截图。这样在文件列表里一眼就能看出哪个是哪个,不用逐个打开。
第四个技巧:日志要记全。每次生成记录输入描述、解析出的参数、生成耗时、输出文件路径。出问题的时候回溯特别方便,也能统计哪些描述容易解析失败,针对性优化 prompt。
7. 我对 text-to-cad 当前能力边界的一些真实看法
用了这段时间,我的整体判断是:text-to-cad 在"规格化、参数化、单零件"这个范围内已经很好用了,生成垫圈、法兰、支架、型材这类零件基本一次成功。但它的边界也很明显:复杂曲面(比如涡轮叶片、有机形状)它搞不定,多零件装配体的约束关系它也处理不好,工程图的标注和公差更是完全不在能力范围内。
所以我的用法是把它当成"参数化建模的加速器",而不是"替代 CAD 工程师的 AI"。它帮你把重复劳动干掉,把想法快速变成可验证的几何,但最终的工程判断、装配设计、工艺考量还是得人来。这个定位想清楚了,用起来就不会有落差。
另外提醒一句:如果你要把生成的模型用于实际生产,务必做几何验证和工艺检查。我见过有人直接把 AI 生成的模型送去打印,结果壁厚只有 0.3mm,一碰就碎。工具再方便,工程常识不能丢。