1. 从一段文字到三维实体:text-to-cad 到底在解决什么问题
第一次听到 "text-to-cad" 这个词,很多人脑子里冒出来的画面是:对着电脑敲一句"给我画一个法兰盘",然后屏幕上自动出现一个可以旋转、可以导出、可以拿去加工的 3D 模型。这个画面听起来像科幻,但它背后其实是一条非常具体的技术链路——把自然语言描述转成结构化的几何参数,再把参数喂给 CAD 内核,最终生成 STEP、URDF 这类标准格式文件。
我接触这个方向是从一个很实际的需求开始的:手头有一批机械零件需要反复微调尺寸,每次改动都要在 CAD 软件里手动拉伸、打孔、倒角,改一个参数要重新走一遍特征树。后来我想,能不能用脚本把"参数"和"几何"解耦,让文字或者表格直接驱动模型生成?这就是 text-to-cad 最朴素的价值——让几何模型的生成过程可编程、可批量、可复现。
它适合谁?三类人最需要:一是做机械设计但想摆脱重复劳动的工程师;二是做机器人仿真、需要批量生成 URDF 模型的开发者;三是想把 AI 能力和传统 CAD 打通的算法同学。哪怕你只是刚学 Python、对 CAD 一知半解,只要理解"参数驱动几何"这个核心思路,就能上手跑通一个最小可用版本。
需要先泼一盆冷水:text-to-cad 不是"说人话就出图"的魔法。真正能落地的方案,是把自然语言先规整成结构化参数(比如 JSON 里的长宽高、孔径、孔位),再交给几何内核去建模。中间那层"语言到参数"的转换,才是这个项目真正的技术含量所在。下面我会把整条链路拆开,从选型、建模、导出到踩坑,一步步讲清楚。
2. 技术选型:为什么是 Python + CadQuery 而不是直接调 CAD 软件
2.1 几何内核的三条路线对比
要生成 CAD 文件,绕不开"几何内核"这个东西。它是真正负责计算曲面、实体、布尔运算的底层引擎。市面上能用的路线大致三条,我列个表对比一下,方便你按自己的场景选。
| 路线 | 代表工具 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 商业 CAD 二次开发 | 各类 CAD 软件的脚本接口 | 功能全、精度高 | 依赖软件安装、授权、跨平台差 | 已有 CAD 工作流、单机使用 |
| 开源脚本建模 | CadQuery、build123d | 纯 Python、可版本控制、可批量 | 复杂曲面能力弱于商业内核 | 参数化零件、批量生成 |
| 网格建模 | trimesh、OpenSCAD | 轻量、易上手 | 输出的是网格不是实体,STEP 支持差 | 3D 打印、可视化 |
我最终选的是CadQuery。原因很直接:它基于 OpenCASCADE 这个成熟的几何内核,能输出真正的 B-rep 实体,导出的 STEP 文件拿去任何主流 CAD 软件里都能正常打开、继续编辑。而 OpenSCAD 那类工具输出的是三角网格,转 STEP 会丢精度、丢拓扑,做机械件基本没法用。
2.2 为什么坚持输出 STEP 而不是 STL
这里有个很多人会踩的坑。STL 是网格格式,本质上是把曲面用无数小三角形逼近。你拿它去 3D 打印没问题,但如果你要把它导入到别的 CAD 软件里继续做特征操作,就会发现它变成了一堆"死面片",没法打孔、没法倒角、没法改尺寸。
STEP(.step / .stp)是实体格式,保留完整的几何拓扑信息。text-to-cad 的产物如果是要进入正式设计流程的,必须优先输出 STEP。我在项目里把 STEP 作为默认导出格式,STL 只作为可选的预览格式。
2.3 URDF 为什么也要一起考虑
关键词里出现了 URDF,这不是偶然。URDF 是机器人领域描述模型的标准格式,它本身不定义几何,而是引用几何文件(通常是 STL 或 DAE)并附加关节、坐标系、惯性参数。所以一条完整的 text-to-cad 链路,往往是:文字描述 → 参数 → 生成几何 → 导出 STL/STEP → 组装成 URDF。
如果你做的是机器人仿真,最终交付物大概率是 URDF 而不是单个零件。这一点在架构设计时就要想清楚,否则后期返工很痛苦。
3. 环境搭建:Python 环境与 CadQuery 安装的完整避坑
3.1 Python 版本选择与虚拟环境
CadQuery 对 Python 版本有要求,我实测下来Python 3.8 到 3.11最稳。太老的版本装不上新版依赖,太新的版本偶尔会遇到某个依赖还没出预编译包,导致要本地编译、耗时且容易失败。
强烈建议用虚拟环境,别往系统 Python 里直接装。原因很简单:CadQuery 依赖的几何库版本比较敏感,一旦和你系统里其他项目的依赖打架,排查起来非常痛苦。
# 创建虚拟环境 python -m venv venv # 激活(Windows) venv\Scripts\activate # 激活(Linux / macOS) source venv/bin/activate激活之后,命令行前面会出现(venv)前缀,说明你已经在隔离环境里了。这一步看着简单,但它是后面所有操作不出乱子的前提。
3.2 安装 CadQuery 的正确姿势
官方推荐用 conda 安装,因为几何内核的二进制依赖用 conda 管理最省心:
conda install -c conda-forge cadquery如果你坚持用 pip,也可以,但要注意它会拉取较大的二进制包:
pip install cadquery注意:pip 安装时如果卡在某个几何库的编译上,八成是缺少系统级的编译工具链。这时候别硬扛,直接换 conda 通道,能省下大半天时间。
装完之后验证一下:
import cadquery as cq print(cq.__version__)能打印出版本号,说明环境通了。如果报ImportError或者找不到某个.dll/.so,基本是二进制依赖没装全,回到 conda 通道重装。
3.3 那些年我踩过的环境坑
第一个坑是中文路径。几何内核在读取临时文件时对非 ASCII 路径支持不好,我有个项目放在带中文的目录下,建模一直报奇怪的 IO 错误,换成纯英文路径立刻就好了。所以项目目录、虚拟环境目录,全部用英文。
第二个坑是多版本 Python 共存。机器上装了 3.8 和 3.11 两个版本,pip和python指向的可能不是同一个解释器,导致"明明装了却 import 不到"。解决办法是永远用python -m pip install而不是裸pip install,保证装到当前解释器里。
第三个坑是权限问题。Linux 下如果用了系统 Python 又没加--user,装包会报权限错误。用虚拟环境就完全没这个问题,这也是我反复强调虚拟环境的原因。
4. 核心链路拆解:从自然语言到结构化参数
4.1 为什么不能直接让大模型输出几何代码
很多人第一反应是:让大模型直接生成 CadQuery 的 Python 代码不就行了?我试过,能跑通简单例子,但一到实际项目就崩。原因有三个:
一是几何代码对参数极其敏感。一个坐标写错一位小数,模型就完全变形,而大模型生成的代码里这种小错误很难被肉眼发现。
二是不可复现。同样的描述,大模型每次生成的代码结构可能都不一样,你没法做版本管理和回归测试。
三是难以校验。你没法在生成阶段判断这个模型是不是符合工程约束(比如壁厚够不够、孔会不会打穿)。
所以我的方案是:让语言模型只负责把自然语言转成结构化参数(JSON),几何生成用固定的、经过测试的模板代码来完成。这样职责清晰,参数可校验,几何可复现。
4.2 结构化参数的字段设计
参数 schema 的设计是整个项目的骨架。以最常见的"带孔法兰盘"为例,我设计的参数字典大概长这样:
{ "part_type": "flange", "outer_diameter": 100.0, "inner_diameter": 40.0, "thickness": 10.0, "bolt_hole_count": 6, "bolt_hole_diameter": 8.0, "bolt_circle_diameter": 80.0, "unit": "mm" }每个字段都有明确含义和单位。单位必须显式声明,这是工程领域的铁律,我见过太多因为单位混乱导致的灾难。字段命名用英文小写下划线,避免歧义。
4.3 参数校验:别让错误参数流到几何层
参数进来之后,第一件事不是建模,是校验。我写了一个校验层,检查几类问题:
- 数值范围:直径不能为负,厚度不能为零。
- 逻辑关系:内径必须小于外径,螺栓圆直径必须在外径和内径之间。
- 孔位冲突:螺栓孔不能互相重叠,也不能超出实体边界。
def validate_flange(p): assert p["outer_diameter"] > p["inner_diameter"] > 0, "内外径关系错误" assert p["thickness"] > 0, "厚度必须为正" assert p["bolt_circle_diameter"] < p["outer_diameter"], "螺栓圆超出外径" # 检查相邻孔是否重叠 import math if p["bolt_hole_count"] > 1: arc = math.pi * p["bolt_circle_diameter"] / p["bolt_hole_count"] assert arc > p["bolt_hole_diameter"], "螺栓孔间距过小,会重叠" return True这段校验代码看着不起眼,但它能在建模之前就把 90% 的无效输入挡掉,省下大量调试时间。校验失败要给出明确的中文错误信息,而不是抛一个裸的 AssertionError,这样调用方一眼就知道哪里错了。
4.4 语言到参数的转换策略
如果输入是自然语言,比如"做一个外径 100、内径 40、厚 10 毫米、6 个 8 毫米螺栓孔的法兰盘",转换层要做的就是抽取这些数值和它们对应的字段。我的做法是先用正则把"数字+单位+名词"的模式抓出来,再用一个字段映射表把中文名词对应到 schema 字段。
对于更复杂的描述,可以引入大模型做抽取,但抽取结果必须经过 4.3 的校验层,不能直接信任。这样即使模型抽错了,也会被校验拦住,不会生成错误模型。
5. 几何建模实战:用 CadQuery 把参数变成实体
5.1 法兰盘建模的完整代码
参数校验通过后,进入建模环节。下面是我实际用的法兰盘生成函数,逻辑清晰,可以直接抄:
import cadquery as cq def build_flange(p): # 基础圆盘 result = ( cq.Workplane("XY") .circle(p["outer_diameter"] / 2) .circle(p["inner_diameter"] / 2) .extrude(p["thickness"]) ) # 螺栓孔 if p["bolt_hole_count"] > 0: result = ( result.faces(">Z") .workplane() .polarArray( radius=p["bolt_circle_diameter"] / 2, startAngle=0, angle=360, count=p["bolt_hole_count"], ) .hole(p["bolt_hole_diameter"]) ) return result这段代码有几个关键点值得说。circle().circle().extrude()是先画两个同心圆再拉伸,CadQuery 会自动把内圆当成孔挖掉,这是它的一个便利特性。polarArray做环形阵列,一次性生成所有螺栓孔,比手动循环优雅得多。.faces(">Z")是选中朝上的那个面作为工作平面,保证孔是沿着 Z 轴打的。
5.2 为什么用 polarArray 而不是循环打孔
有人会想,我 for 循环一个个打孔不行吗?行,但没必要。polarArray是内核级的阵列操作,一次调用完成所有孔的定位和布尔运算,代码更短、执行更快、也更不容易出错。手动循环要自己算每个孔的圆心坐标,涉及三角函数,容易写错角度单位(弧度还是角度)。
提示:CadQuery 里角度默认是度,不是弧度。这一点和很多数学库相反,第一次用很容易搞混。
5.3 建模过程中的常见几何错误
第一个常见错误是布尔运算失败。比如孔的位置刚好落在实体边缘上,或者两个孔重叠,内核算不出来就会报错。解决办法就是前面说的参数校验,提前把这种输入挡掉。
第二个错误是工作平面选错。.faces(">Z")选的是 Z 方向最大的面,如果你建模时坐标系搞反了,可能选到背面,孔就打反了。调试时可以用.faces(">Z").val()打印一下面的信息确认。
第三个错误是单位不一致。CadQuery 内部没有单位概念,你输入的数字就是它理解的尺寸。如果你一会儿用毫米一会儿用米,模型尺寸会差一千倍。所以我在参数层强制统一单位,建模函数只认毫米。
5.4 从单零件到装配体
实际项目里很少只生成一个零件。比如一个减速箱,有壳体、端盖、轴、齿轮。这时候就要用 CadQuery 的装配功能:
assy = ( cq.Assembly() .add(build_flange(p1), name="flange", color=cq.Color("gray")) .add(build_shaft(p2), name="shaft", color=cq.Color("steelblue")) )装配体可以整体导出,也可以分别导出每个零件。做机器人仿真时,装配体的每个零件往往对应 URDF 里的一个 link,所以命名要规范,方便后续映射。
6. 导出与格式转换:STEP、STL、URDF 怎么选怎么用
6.1 STEP 导出的参数细节
导出 STEP 很简单:
cq.exporters.export(result, "flange.step")但有几个细节要注意。CadQuery 的 STEP 导出默认是 AP214 协议,兼容性最好。如果你需要带颜色信息,可以传opt={"write_pcurves": False}之类的参数微调,但大多数场景默认值就够用。
导出后一定要实际打开验证。我习惯用任意一款主流 CAD 软件打开生成的 STEP,检查实体是否完整、孔位是否正确、有没有多余的碎面。自动化测试里也可以写一个回读校验:把 STEP 重新读进来,比对体积或包围盒尺寸是否和预期一致。
6.2 STL 导出的精度控制
STL 是网格格式,导出时要控制网格精度,否则要么文件巨大,要么曲面变成多边形:
cq.exporters.export( result, "flange.stl", tolerance=0.01, angularTolerance=0.1 )tolerance是线性偏差,越小网格越密;angularTolerance是角度偏差。对于机械件,我一般用 0.01 毫米的线性偏差,兼顾精度和文件大小。做 3D 打印预览可以放宽到 0.05。
6.3 URDF 组装的关键字段
URDF 是 XML 格式,核心结构是 link 和 joint。每个 link 引用一个几何文件,并定义惯性参数:
<link name="flange_link"> <visual> <geometry> <mesh filename="flange.stl" scale="1 1 1"/> </geometry> </visual> <collision> <geometry> <mesh filename="flange.stl"/> </geometry> </collision> <inertial> <mass value="0.5"/> <inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/> </inertial> </link>这里最容易出错的是惯性参数。质量、转动惯量如果乱填,仿真时物体会表现得很诡异,比如轻轻一碰就疯狂旋转。惯性参数最好从 CAD 软件里根据材料密度算出来,或者用几何近似公式估算。URDF 里的 mesh 路径是相对路径,导入仿真环境时路径不对会直接加载失败,这一点要特别注意。
6.4 导入仿真环境时的坐标系对齐
把 URDF 导入仿真环境(比如各类机器人仿真平台)时,最常见的坑是坐标系不一致。CAD 里建模时用的坐标系,和 URDF 里定义的 link 坐标系,往往需要做一次变换。如果没对齐,模型会飘在空中或者陷进地面。
我的做法是在建模阶段就统一约定:所有零件以自身几何中心或某个基准点为原点,装配时通过 joint 的 origin 字段做平移和旋转。这样每个零件的局部坐标系是干净的,装配关系全部在 URDF 里表达,职责清晰。
7. 批量生成与自动化:把重复劳动彻底交给脚本
7.1 参数表格驱动批量建模
text-to-cad 真正的威力在批量。假设你有一张 Excel 表,每行是一组法兰参数,要生成几十个模型。用脚本读表、循环建模、批量导出,几分钟搞定:
import pandas as pd df = pd.read_excel("flange_params.xlsx") for idx, row in df.iterrows(): p = row.to_dict() validate_flange(p) model = build_flange(p) cq.exporters.export(model, f"output/flange_{idx}.step")这段代码的价值在于:参数和代码分离。以后要改尺寸,改 Excel 就行,不用碰代码。这也是我一直强调"参数驱动"的原因。
7.2 批量任务的异常处理
批量跑最怕的是中途某个参数出错,整个任务崩掉。所以循环里要加异常捕获,把失败的记录单独记下来:
failed = [] for idx, row in df.iterrows(): try: p = row.to_dict() validate_flange(p) model = build_flange(p) cq.exporters.export(model, f"output/flange_{idx}.step") except Exception as e: failed.append((idx, str(e))) continue跑完之后打印failed列表,就知道哪些行有问题、问题是什么。这样一次批量任务不会因为个别坏数据全军覆没。
7.3 和 CAD 软件工作流的衔接
生成的 STEP 文件最终往往要回到 CAD 软件里做后续处理,比如出工程图、做装配、加标注。这时候文件命名规范就很重要。我的命名规则是零件类型_关键尺寸_版本.step,比如flange_D100_d40_v2.step。这样在文件夹里一眼就能找到需要的文件,不用一个个打开看。
如果团队里有人习惯用 CAD 的批量处理功能,可以把生成的 STEP 统一放到一个目录,用软件的批量导入功能一次性加载。这比手动一个个打开效率高得多。
8. 实战踩坑记录:那些文档里不会写的问题
8.1 几何内核的浮点精度陷阱
有一次我生成一批零件,尺寸参数完全一样,但导出的 STEP 文件大小却不一样,有的还报了几何错误。排查半天发现是浮点精度问题:某些参数经过多次运算后产生了极小的误差(比如 10.0000000001),导致内核在做布尔运算时判定失败。
解决办法是在参数进入建模前做一次数值规整,把尺寸四舍五入到合理的小数位(比如 0.001 毫米):
def round_params(p, ndigits=3): return {k: round(v, ndigits) if isinstance(v, float) else v for k, v in p.items()}这个操作看着粗暴,但工程上完全够用,因为机械加工精度本来也到不了微米以下。
8.2 大模型抽取参数的稳定性问题
用大模型做自然语言抽取时,同一个描述多跑几次,结果可能不一样。有时候把"外径 100"抽成 100,有时候抽成 100.0,有时候甚至抽错字段。我的应对策略是:抽取结果必须过校验层,且关键字段做二次确认。比如数值字段,如果模型返回的是字符串,先尝试转 float,转不了就报错。
另外,给大模型的提示词里要把 schema 写清楚,明确每个字段的含义、单位、取值范围。提示词越具体,抽取越稳定。我一般会把 schema 直接以 JSON 示例的形式放进提示词,让模型照着格式输出。
8.3 导出文件在别的软件里打不开
这个问题我遇到过两次。一次是 STEP 文件在某个 CAD 软件里打开后是空白的,后来发现是导出时用了非标准的协议选项。另一次是 STL 文件在切片软件里显示破面,原因是网格精度设得太粗,曲面逼近误差过大。
经验就是:导出格式用最通用的默认配置,别乱改高级选项。STEP 用默认 AP214,STL 用合理的 tolerance,基本不会出问题。如果对方软件打不开,先怀疑是不是自己用了非标准选项,而不是对方软件有问题。
8.4 内存与性能:大批量生成时的优化
生成几百个复杂零件时,内存会涨得很快。原因是 CadQuery 的每个模型对象都持有几何内核的资源,不主动释放就会堆积。解决办法是分批处理,每处理完一批就清空一次:
import gc for i, batch in enumerate(batches): for p in batch: model = build_flange(p) cq.exporters.export(model, f"output/part_{i}.step") del model gc.collect()del model加上gc.collect()能显著降低内存峰值。另外,如果不需要装配,就别构建 Assembly 对象,单零件直接导出更省资源。
9. 关于 text-to-cad 后续能怎么扩展
跑通基础链路之后,这个项目还有不少可以深挖的方向。比如把参数 schema 做得更通用,支持更多零件类型(轴、齿轮、支架),形成一个零件库;比如接入更规范的自然语言接口,让非工程背景的人也能描述需求;再比如把生成的模型直接对接下游的仿真、分析流程,形成"描述—建模—验证"的闭环。
我自己在实际操作中的体会是:text-to-cad 的核心难点从来不在"生成几何",而在"把模糊的人类描述翻译成精确的工程参数"。几何生成有成熟的内核可以依赖,但语言到参数这一层,需要大量的领域知识和校验逻辑。把这一层做扎实,整个系统才真正可用。如果你也在做类似的东西,建议先把参数 schema 和校验层设计好,这部分的投入回报比最高。