1. 从一句话到三维实体:text-to-cad 到底在解决什么问题
第一次听到 "text-to-cad" 这个词,很多人脑子里浮现的画面大概是:对着电脑说一句"给我画个法兰盘",屏幕上就自动长出一个带螺栓孔的零件。这个想象不算离谱,但真正落地的时候,它解决的问题比"省几下鼠标点击"要深得多。
传统 CAD 工作流的本质是人肉翻译:需求方用自然语言描述一个零件("直径 80、厚 10、中心一个 30 的通孔、四周均布 6 个 M6 沉头孔"),工程师在脑子里把这个描述转成几何约束,再用手在草图里一条线一条线画出来,最后拉伸、打孔、倒角。这个链条里,最贵的一环不是画图本身,而是从语言到几何的那次翻译——它依赖经验、容易出错、而且几乎无法批量复用。
text-to-cad 想干的事情,就是把这次翻译交给程序。输入是一段结构化的文本描述,输出是标准的 CAD 几何文件,常见的目标格式包括STEP(通用三维交换格式)、URDF(机器人描述格式,带运动学信息)、以及G-code(数控加工指令)。这三个格式分别对应三类下游场景:STEP 给通用 CAD/CAM 软件做后续编辑和出图,URDF 给机器人仿真环境做装配和运动验证,G-code 直接驱动机床把零件切出来。
所以这个项目的核心价值不是"炫技",而是把重复性的参数化建模变成可编程、可批量、可版本管理的流程。适合谁来参考?三类人最受益:一是经常要做系列化零件的机械工程师(同一结构改尺寸改几十遍那种),二是做机器人仿真、需要批量生成连杆和关节模型的开发者,三是想把设计流程接进自动化管线、用脚本驱动 CAD 的技术人员。哪怕你只是 CAD 初学者,理解这套思路也能帮你建立"参数化思维",比死记命令有用得多。
下面我会把这条链路拆开讲:文本怎么变成几何、STEP/URDF/G-code 各自怎么生成、批量处理怎么做、以及我在实操里踩过的那些坑。
2. 文本描述怎么变成几何:解析层的设计取舍
2.1 为什么不能直接让大模型吐 STEP 文件
一个很自然的想法是:既然有大语言模型,直接让它输出 STEP 文件内容不就行了?我试过,结论是基本不可行。原因在于 STEP 文件(ISO 10303-21)是一种极其啰嗦的边界表示格式,一个简单的圆柱体在 STEP 里可能是几百行CARTESIAN_POINT、DIRECTION、CYLINDRICAL_SURFACE的实体引用,而且实体之间的引用关系必须严格自洽,ID 不能错、拓扑不能断。语言模型生成这种长程强约束的结构化文本,出错率极高,而且一旦某个引用 ID 错了,整个文件在 CAD 软件里直接打不开,你连错在哪都难找。
正确的做法是分层:让语言模型只负责它擅长的部分——把自然语言解析成结构化的参数字典(JSON),几何生成交给确定性的建模内核。这样每一层都可测试、可调试、可替换。
2.2 参数抽取:从"直径80厚10"到结构化 JSON
解析层的核心任务是把一段人话拆成机器能用的字段。我实际用的方案是"规则 + 模型兜底"的混合策略,纯规则太脆,纯模型不稳定。
先看一个典型的输入:
法兰盘,外径 80mm,厚度 10mm,中心通孔直径 30mm, 均布 6 个 M6 沉头孔,分布圆直径 60mm,材料 6061 铝目标是抽出这样的结构:
{ "type": "flange", "outer_diameter": 80.0, "thickness": 10.0, "center_hole_diameter": 30.0, "bolt_holes": { "count": 6, "thread": "M6", "pattern_circle_diameter": 60.0, "type": "countersunk" }, "material": "AL6061", "unit": "mm" }规则层负责抓"数字 + 单位 + 关键词"的组合,比如正则匹配外径\s*(\d+(?:\.\d+)?)\s*(mm|cm|m)?。模型层负责处理规则抓不到的表述,比如"稍微倒个角""孔别太靠边"这种模糊描述,让它映射到默认值或追问。这里有个关键设计:单位必须显式归一化。我见过太多因为 mm 和 inch 混用导致零件尺寸差 25.4 倍的惨案,所以解析完第一件事就是统一到毫米,并在 JSON 里保留unit字段做审计。
提示:解析层一定要输出"置信度"或"缺失字段列表"。当关键尺寸缺失时,宁可报错让用户补全,也不要瞎猜一个默认值——猜错的几何比没有几何更危险,因为它可能被直接送进加工。
2.3 几何内核选型:为什么我最终选了 CadQuery 而不是直接调商业 API
几何生成这一层,可选路线有几条:调用商业 CAD 的 API(如某些软件的二次开发接口)、用开源内核(OpenCASCADE)、或者用基于 OpenCASCADE 封装的高层库(CadQuery、build123d)。
我最终选的是CadQuery,理由有三条。第一,它是 Python 原生,和解析层、批量脚本能无缝衔接,不用跨语言传参。第二,它基于 OpenCASCADE,几何内核成熟,导出的 STEP 是真正的 B-rep 实体,不是网格,能被任何主流 CAD 软件正常打开和编辑。第三,它的代码即模型(code-CAD)范式天然适合参数化——一个法兰盘就是一个函数,改参数就出新零件。
对比一下几条路线的取舍:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 商业 CAD 二次开发 | 功能全、出图规范 | 授权成本高、脚本环境封闭、难批量 | 企业内部已有授权 |
| OpenCASCADE 裸用 | 完全可控、免费 | 学习曲线陡、样板代码多 | 需要深度定制内核 |
| CadQuery / build123d | 上手快、Python 生态、导出标准 | 复杂曲面能力有限 | 参数化零件、批量生成 |
| 网格类库(如 trimesh) | 轻量、渲染快 | 不是实体、无法直接加工 | 可视化、3D 打印预览 |
对于 text-to-cad 这种"以参数化规则零件为主"的场景,CadQuery 是性价比最高的选择。它的核心建模逻辑就三句话:建草图(Workplane)、加约束、做布尔运算(拉伸、切除、倒角)。
2.4 一个法兰盘的完整生成代码
把上面的 JSON 喂给建模函数,核心代码大概长这样:
import cadquery as cq import math def build_flange(p): od = p["outer_diameter"] th = p["thickness"] ch = p["center_hole_diameter"] bh = p["bolt_holes"] pcd = bh["pattern_circle_diameter"] n = bh["count"] hole_d = 6.6 # M6 通孔按 6.6 留间隙 # 基础圆盘 part = ( cq.Workplane("XY") .circle(od / 2) .extrude(th) ) # 中心通孔 part = part.faces(">Z").workplane().hole(ch) # 均布螺栓孔 pts = [ (pcd / 2 * math.cos(2 * math.pi * i / n), pcd / 2 * math.sin(2 * math.pi * i / n)) for i in range(n) ] part = ( part.faces(">Z").workplane() .pushPoints(pts) .hole(hole_d) ) # 上下边缘倒角 part = part.edges("|Z").chamfer(0.5) return part result = build_flange(parsed_json) cq.exporters.export(result, "flange.step")这段代码里有两个容易被忽略的细节。一是螺栓孔直径不是螺纹公称直径。M6 的螺纹外径是 6mm,但通孔要留间隙,通常取 6.6mm(中等装配),如果要做精密定位可能取 6.4mm,做松装配取 7mm。这个值直接决定装配能不能装上,是新手最容易翻车的地方。二是倒角放在最后做,因为布尔运算之后再倒角,边才是最终边;如果先倒角再打孔,孔口的新边就没有倒角,看起来会很突兀。
3. STEP、URDF、G-code:三种输出格式各自的坑
3.1 STEP 导出:为什么你的文件在别人电脑上打不开
STEP 是 text-to-cad 最常用的输出,但导出这一步的坑比想象中多。最常见的问题是单位。STEP 文件内部有自己的单位声明,CadQuery 默认按毫米导出,但如果你在建模时用了英寸思维(比如把 80 当成 80 英寸),导出的文件在别人那里就是 2 米大的怪物。我的做法是在导出前强制断言一次包围盒尺寸,超出合理范围就报警。
第二个坑是精度与文件体积的平衡。STEP 导出时可以设置线性公差和角度公差,公差越小文件越大。对于普通机械零件,线性公差设 0.01mm 足够;如果你做的是光学件或者需要高精度曲面,可能要设到 0.001mm。但公差设太小会导致文件里实体数量爆炸,打开卡顿。我一般先用默认值导出,如果下游反馈"面有缝隙",再回头收紧公差。
第三个坑是命名。STEP 里的实体、面、边都可以带名字,但很多导出器默认不写。如果你的下游流程需要按名字识别特征(比如自动化装配),一定要在建模时给关键面打标签,导出时保留。否则下游拿到的是一个"无名实体堆",只能靠几何位置猜,非常痛苦。
3.2 URDF 生成:几何只是第一步,关节才是灵魂
URDF(Unified Robot Description Format)是机器人领域的标准描述格式,它和纯几何的 STEP 有本质区别:URDF 描述的是带运动学关系的装配体。一个机械臂的 URDF 里,每个连杆(link)是几何体,每个关节(joint)定义了父子连杆之间怎么相对运动——是旋转还是平移、绕哪个轴、行程范围多少。
text-to-cad 生成 URDF 的难点不在几何,而在关节信息的推断。文本里说"两段连杆用铰链连接",程序得知道:铰链轴在哪、旋转范围多少、两个连杆的坐标系原点怎么对齐。这些信息往往在自然语言里是缺失的,所以我的做法是要求输入文本必须显式描述关节,或者提供一个关节参数表。
一个典型的 URDF 片段:
<link name="link1"> <visual> <geometry> <mesh filename="link1.stl" scale="0.001 0.001 0.001"/> </geometry> </visual> <collision> <geometry> <mesh filename="link1.stl" scale="0.001 0.001 0.001"/> </geometry> </collision> <inertial> <mass value="0.5"/> <inertia ixx="0.001" iyy="0.001" izz="0.001" ixy="0" ixz="0" iyz="0"/> </inertial> </link> <joint name="joint1" type="revolute"> <parent link="link1"/> <child link="link2"/> <axis xyz="0 0 1"/> <limit lower="-1.57" upper="1.57" effort="10" velocity="1.0"/> </joint>这里有几个实操要点。第一,mesh 的 scale。URDF 里长度单位是米,而 CAD 建模通常是毫米,所以 mesh 引用时要加scale="0.001 0.001 0.001",否则你的机器人会大 1000 倍。这个坑我踩过,导入仿真环境后整个模型飞出视野,找了半天才发现是单位问题。第二,inertial 不能省。很多人只写 visual 和 collision,结果仿真时物体没有质量,物理引擎直接报错或者行为诡异。质量可以估算,惯量张量哪怕用简化公式(把连杆近似成圆柱或长方体)也比不写强。第三,collision 几何可以比 visual 简化。visual 用精细网格好看,collision 用包围盒或简化凸包能大幅提升仿真速度,这是机器人仿真里的常规优化。
3.3 G-code:从几何到刀路,中间隔着一整个 CAM
G-code 是数控机床能直接执行的指令,但从 STEP 到 G-code 不是一步转换,中间必须经过 CAM(计算机辅助制造)。这一步 text-to-cad 通常不直接做,而是把 STEP 交给 CAM 软件或库(如 FreeCAD 的 Path 工作台、或专门的 CAM 引擎)生成刀路。
为什么不能直接生成 G-code?因为刀路依赖太多几何之外的信息:用什么刀具、刀具直径多少、切削深度、进给速度、主轴转速、是粗加工还是精加工、材料是什么。这些参数文本里往往没有,需要工艺知识。所以合理的架构是:text-to-cad 负责生成准确的 STEP 几何,CAM 环节单独处理,或者提供一个"工艺参数模板"让用户填。
如果你确实想打通到 G-code,我的建议是先用 FreeCAD 的 Path 工作台做半自动生成:导入 STEP,选刀具,设参数,生成刀路,导出 G-code。全自动的难度在于工艺决策,那是个比几何生成复杂得多的问题,不建议在 text-to-cad 项目里硬啃。
4. 批量生成与参数扫描:让脚本替你干重复活
4.1 参数化建模的真正威力在批量
单个零件生成只是入门,text-to-cad 真正的价值在批量。假设你要做一系列法兰盘,外径从 50 到 200,每 10mm 一档,共 16 个规格。手工建模要画 16 次,用脚本就是一个循环:
import cadquery as cq base = { "thickness": 10.0, "center_hole_diameter": 30.0, "bolt_holes": { "count": 6, "pattern_circle_diameter": 60.0, "type": "countersunk" }, "unit": "mm" } for od in range(50, 201, 10): p = dict(base) p["outer_diameter"] = float(od) # 分布圆随外径缩放,保持边距合理 p["bolt_holes"] = dict(base["bolt_holes"]) p["bolt_holes"]["pattern_circle_diameter"] = od * 0.75 part = build_flange(p) cq.exporters.export(part, f"flange_D{od}.step")这段代码里有个设计决策值得说:分布圆直径不是固定值,而是随外径缩放。如果固定 60mm,外径 200 的法兰盘螺栓孔会挤在中心,边缘一大圈没材料,既不美观也不合理。按 0.75 倍外径缩放是个经验值,保证孔到边缘有足够边距。这种"派生参数"的逻辑,正是参数化建模比手工建模强的地方——规则一旦定好,所有规格自动一致。
4.2 参数扫描与设计表
更进一步,你可以把参数组合做成一张 CSV 设计表,脚本读表批量生成。这在做系列化产品时特别有用,设计表本身就是可版本管理的"设计意图"。
| 规格代号 | 外径 | 厚度 | 中心孔 | 螺栓数 | 分布圆 | 材料 |
|---|---|---|---|---|---|---|
| FL-050 | 50 | 8 | 20 | 4 | 37.5 | AL6061 |
| FL-080 | 80 | 10 | 30 | 6 | 60 | AL6061 |
| FL-120 | 120 | 12 | 45 | 8 | 90 | AL6061 |
| FL-200 | 200 | 16 | 80 | 12 | 150 | AL6061 |
脚本读这张表,逐行生成 STEP,文件名带规格代号。这样一套流程下来,几十个规格几分钟跑完,而且每个文件的建模逻辑完全一致,不会出现"这个画的时候手抖多打了个孔"的问题。
4.3 批量生成时的资源管理
批量跑的时候有个现实问题:内存和临时文件。CadQuery 每次建模都会在内存里构建 B-rep 结构,如果循环几百次不释放,内存会涨到爆。我的做法是每生成一批(比如 20 个)就显式清理一次,或者干脆把批量任务拆成多个子进程跑,每个进程处理一批,跑完退出释放内存。
另外,导出路径要规划好。我习惯按输出目录/规格代号/文件名.step的结构组织,方便下游按规格检索。如果下游是自动化管线,还要同时导出一份清单文件(JSON 或 CSV),记录每个文件的参数和生成时间,方便追溯。
5. 实操中踩过的坑与排查链路
5.1 孔打在了错误的面:工作平面选择问题
最早做的时候,我遇到过一个诡异现象:法兰盘生成出来,螺栓孔不在上表面,而是跑到了侧面,整个零件像被扎了一圈针。排查了半天,问题出在workplane()的选择上。
CadQuery 里.faces(">Z")表示选 Z 方向最靠上的那个面,.workplane()在这个面上建立新的工作平面。但如果前面的布尔运算改变了面的方向,或者有多个面满足条件,选中的可能不是你想要的那个。我的修复方式是显式指定选择器,比如.faces(cq.selectors.NearestToPointSelector((0, 0, th))),直接选离某个点最近的面,避免歧义。
这个坑的教训是:在 code-CAD 里,选择器比几何本身更容易出错。几何是确定的,但"选哪个面"依赖选择器的语义,一旦模型复杂起来,隐式选择很容易选错。所以我现在写建模代码,凡是涉及面/边选择的,都尽量用显式、可验证的选择器,并在关键步骤后打印一下选中对象的数量做校验。
5.2 导出的 STEP 在某个软件里显示为空心
有次下游反馈,我导出的 STEP 在他们的软件里打开是空心的,只有壳没有实体。我这边用 FreeCAD 打开明明是实心的。排查下来,问题出在导出精度和软件容差的差异上。我的模型里有个很小的倒角(0.1mm),导出时用的默认公差比较大,导致那个倒角面在转换时退化,B-rep 的拓扑出现了微小裂缝。我的软件容差松,自动缝合了;他们的软件容差严,缝不上,就显示成空心。
修复方案有两个:一是把那个倒角改大一点(0.5mm),二是导出时收紧公差。我两个都做了,问题消失。这件事让我养成了一个习惯:导出后一定用至少两个不同的软件打开验证,一个宽松一个严格,能提前发现这类兼容性问题。
5.3 URDF 导入仿真环境后模型乱飞
前面提过单位问题,这里展开说排查链路。现象是:URDF 导入仿真环境后,机械臂的连杆位置全乱,有的飞出去,有的叠在一起。排查步骤:
- 先看 URDF 里的 mesh scale,确认是不是 0.001。发现没写 scale,默认按米处理,而 mesh 是毫米建模的,大了 1000 倍。
- 加上 scale 后,位置还是不对。检查 joint 的 origin,发现父子连杆的坐标系原点没有对齐,joint 的
origin xyz写的是相对位置,但我按绝对位置填了。 - 修正 origin 后,模型位置对了,但一仿真就抖动。检查 inertial,发现惯量张量填的是 0,物理引擎算不出稳定解。用简化公式补上惯量后,仿真稳定。
这条链路说明:URDF 的问题往往不是单一原因,而是单位、坐标系、物理属性三层叠加。排查时要一层一层验证,别指望一次改对。
5.4 批量生成时文件名冲突覆盖
这个坑比较低级但很常见:批量循环里文件名只用了外径,结果不同厚度但同外径的规格互相覆盖,最后只剩最后一个。修复很简单,文件名带上所有区分参数,或者直接用设计表里的规格代号。我现在一律用规格代号做文件名,因为它是设计表的主键,天然唯一。
6. 把 text-to-cad 接进实际工作流的几点经验
6.1 输入文本的规范化比模型能力更重要
做了这么多轮,我最大的体会是:text-to-cad 的瓶颈往往不在生成端,而在输入端的规范化。自然语言太自由了,"大一点的孔""差不多居中"这种描述,再强的模型也没法稳定处理。所以实际项目里,我倾向于定义一个受控的输入模板,要求用户按字段填,而不是随便写一段话。
比如法兰盘的输入模板:
类型: 法兰盘 外径: 80 mm 厚度: 10 mm 中心孔: 30 mm 螺栓孔: 6 x M6, 分布圆 60 mm, 沉头 材料: 6061 铝这种半结构化的输入,解析准确率能从"看运气"提升到"基本稳定"。语言模型在这里的角色是容错解析器——用户填得不太规范时帮忙纠正,而不是从零理解一段散文。
6.2 版本管理:几何也要进 Git
code-CAD 的一大好处是几何可以进版本控制。传统 CAD 的二进制文件没法 diff,改了什么全靠人记。而 text-to-cad 的输入是文本、建模是代码,两者都能进 Git。每次改参数、改建模逻辑,都有清晰的提交记录。
我的做法是:输入参数(JSON/CSV)和建模脚本分开管理,脚本里不硬编码任何具体尺寸,全部从参数读。这样改一个尺寸只动参数文件,改建模逻辑只动脚本,职责清晰。生成的 STEP 文件本身不进 Git(二进制、体积大),但生成脚本和参数进,需要时随时能重新生成。
6.3 验证环节不能省
自动生成的几何,一定要有自动验证。我常用的几个检查:
- 包围盒检查:生成后的零件包围盒尺寸是否和输入参数一致,防止单位错误或缩放错误。
- 体积检查:估算体积是否在合理范围,防止布尔运算失败导致实体缺失。
- 水密性检查:STEP 是否是有效实体(solid),不是壳(shell)或面片。
- 孔位检查:螺栓孔的实际位置和数量是否和参数一致,可以用几何查询验证。
这些检查写起来不复杂,但能拦住 90% 的低级错误。尤其是批量生成时,没有自动验证,你根本不知道哪几个文件是坏的。
6.4 关于"完全自动"的预期管理
最后说点实在的。text-to-cad 能做到"一句话出零件",但那个"一句话"必须是结构良好、信息完整的一句话。指望用户随便说一句模糊需求就出可加工的零件,目前不现实,也不安全——几何错误可能导致加工报废甚至安全事故。
我的定位是:text-to-cad 是效率工具,不是替代工程师的工具。它把工程师从重复的建模劳动里解放出来,但设计意图的定义、关键尺寸的确认、生成结果的审核,仍然需要人。把预期放在"批量、参数化、可复现"上,这个项目的价值就非常实在;把预期放在"全自动无人设计"上,那大概率会失望。
我在实际使用中发现,最舒服的工作流是:工程师定义参数表和建模规则,脚本批量生成,工程师抽查验证,下游直接用。这个流程里,人的价值集中在"定义规则"和"审核结果"两件事上,中间的重复劳动全部自动化。这比追求"全自动"务实得多,也可靠得多。