1. 从一句话到三维模型:text-to-cad 到底在解决什么问题
第一次听到 "text-to-cad" 这个词,很多人脑子里浮现的画面大概是:对着电脑说一句"给我画个法兰盘",屏幕上就自动长出一个带螺栓孔的零件。这个想象不算离谱,但真正落地的时候,它解决的其实是一个更朴素、也更痛的问题——从"想法"到"可编辑几何体"之间的那道鸿沟。
传统 CAD 工作流是这样的:你脑子里有个结构,先想清楚尺寸、约束、基准面,然后打开软件,一个草图一个草图地画,一次拉伸一次倒角地做。这个过程对熟练工程师来说可能只要十几分钟,但对刚入门的人、对只想快速验证一个结构概念的人来说,门槛高得吓人。更麻烦的是,当你需要批量生成几十上百个相似但参数不同的模型时,纯手工操作几乎不可行。
text-to-cad 的核心价值,就是把这套"人肉翻译"的过程交给程序。你用自然语言或者结构化文本描述一个形状,系统把它翻译成 CAD 能识别的几何数据,最终输出成 STEP、URDF、G-code 这类下游能直接消费的格式。它不是一个要取代工程师的工具,而是一个把重复劳动和概念验证成本压到极低的加速器。
这篇文章适合几类人看:做机械设计想提效的工程师、搞机器人仿真需要批量生成 URDF 的开发者、做 3D 打印想快速出 G-code 的创客,以及任何对"文本驱动几何"这个方向好奇的技术人。我会把这条链路从输入解析、几何生成、格式导出到实际踩坑,完整拆一遍,尽量让你看完就能自己动手跑通一个最小可用版本。
需要先说明一点:text-to-cad 目前不是一个单一的标准软件,而是一类技术方案的统称。不同实现路线差别很大,有的基于参数化建模内核,有的基于网格生成,有的干脆调用大模型输出建模脚本。下面讲的内容,是基于这类方案最常见的工程实践做的梳理和补全,具体到你用的工具,细节可能需要微调。
2. 文本描述是怎么变成几何体的:三层翻译链路拆解
要理解 text-to-cad 为什么有时候好用、有时候离谱,得先搞清楚它内部到底做了哪几件事。我把它拆成三层:语义解析层、几何构建层、格式序列化层。这三层任何一层出问题,最终结果都会跑偏。
2.1 语义解析层:把"人话"拆成结构化参数
这一层的任务是把 "一个直径 50mm、厚 10mm、中心带 20mm 通孔的圆盘" 这种描述,转成程序能处理的参数集合。听起来简单,实际上坑最多。
自然语言里充满了歧义。"带孔的圆盘"——孔在中心还是边缘?几个孔?通孔还是盲孔?"厚 10mm" 是总厚度还是单边壁厚?人类工程师靠经验和上下文能补全,但程序不行。所以成熟的 text-to-cad 方案,通常不会直接吃纯自然语言,而是约定一套半结构化的描述语法,或者用大模型先把自然语言规整成结构化 JSON,再交给几何引擎。
我实测下来比较稳的做法是定义一个中间 schema,比如:
{ "primitive": "cylinder", "params": { "diameter": 50, "height": 10, "unit": "mm" }, "features": [ { "type": "hole", "subtype": "through", "diameter": 20, "position": [0, 0] } ] }这个 schema 就是"人话"和"几何"之间的合同。大模型负责把自然语言填进这个合同,几何引擎只认合同不认人话。这样做的好处是错误可定位——如果模型生成错了,你能一眼看出是哪个字段理解偏了,而不是面对一个莫名其妙的形状干瞪眼。
提示:如果你打算自己搭一套,强烈建议先定 schema 再接大模型。反过来做的话,你会陷入"模型输出格式天天变、下游解析天天崩"的泥潭。
2.2 几何构建层:参数化内核 vs 网格生成
拿到结构化参数后,就要真正"造"出几何体了。这里有个关键分叉:用参数化建模内核,还是用网格生成。
参数化内核(典型代表是 OpenCASCADE 这类)生成的是 B-Rep(边界表示)几何,特点是精确、可编辑、能导出标准 STEP。你给它一个直径 50 的圆柱,它内部记录的就是"半径 25 的解析曲面",不是一堆三角形。这种几何拿去加工、拿去装配、拿去出工程图都没问题。
网格生成(比如直接生成 STL 那种三角面片)则相反,它快、简单、对渲染友好,但精度是近似的,圆会变成多边形,而且几乎没法反向编辑。你没法对一个 STL 说"把这个孔从 20 改成 25",只能重新生成。
所以选型逻辑很清晰:
| 需求场景 | 推荐路线 | 原因 |
|---|---|---|
| 要出 STEP、要后续编辑 | 参数化内核 | 精确几何,支持特征修改 |
| 只做可视化、3D 打印预览 | 网格生成 | 快,渲染友好 |
| 机器人仿真要 URDF | 参数化内核 + 碰撞体简化 | 视觉几何要精确,碰撞体可简化 |
| 要出 G-code 加工 | 参数化内核 | 刀路规划需要精确曲面 |
我个人的经验是:只要下游涉及"精确"两个字,就别图省事用网格。曾经为了快,用网格方案生成了一批零件去做装配验证,结果因为圆孔被离散成多边形,装配间隙算出来全是错的,返工重做花的时间远超当初省下的。
2.3 格式序列化层:STEP、URDF、G-code 各管一段
几何造出来之后,要变成下游能用的文件。这三种格式代表了三个完全不同的下游:
STEP是 CAD 界的通用交换格式,几乎所有主流 CAD 软件都能读写。它的核心价值是跨软件、跨平台地传递精确几何。text-to-cad 输出 STEP,意味着你生成的模型可以无缝进入 SolidWorks、中望 CAD、FreeCAD 这些软件继续深化。STEP 里不仅有几何,还有装配层级、颜色、图层这些信息。
URDF是机器人领域的描述格式,全称统一机器人描述格式。它描述的不只是几何,还有运动学结构——哪个是连杆、哪个是关节、关节怎么转、坐标系怎么挂。text-to-cad 生成 URDF 时,重点不是把零件画得多漂亮,而是把 link 和 joint 的层级关系、惯性参数、碰撞体配好。这里有个常见误区:很多人以为 URDF 里的几何越精细越好,其实碰撞检测用的碰撞体应该尽量用简单形状(圆柱、盒子),精细几何只放在视觉显示里,否则仿真会慢到没法用。
G-code是数控加工和 3D 打印的指令语言,本质是一串"走到哪、走多快、挤多少料"的坐标指令。从几何到 G-code 中间还隔着切片或刀路规划这一步,text-to-cad 通常只负责把几何准备好,切片交给专门的工具。但如果你要做的是"文本描述直接出打印文件",那这条链路就得打通。
3. 搭一套最小可用的 text-to-cad 流程:从环境到跑通
光讲原理没意思,这一节我带你走一遍最小可用流程。目标很明确:输入一段结构化文本,输出一个能打开的 STEP 文件。工具链我用 Python + CadQuery(一个基于 OpenCASCADE 的参数化建模库),选它的理由是纯代码、跨平台、社区活跃,而且对 STEP 导出支持很好。
3.1 环境准备里最容易翻车的几个点
先说安装。CadQuery 的安装是新手第一个坎,因为它依赖 OpenCASCADE 的底层库,不同系统表现不一样。
# 推荐用 conda 装,比 pip 省心很多 conda create -n t2cad python=3.10 conda activate t2cad conda install -c conda-forge cadquery为什么推荐 conda 而不是 pip?因为 CadQuery 依赖的 OCP(OpenCASCADE 的 Python 绑定)在 pip 下经常出现编译问题,尤其是 Windows 上,缺 C++ 运行库、缺编译工具链的报错能折腾一下午。conda-forge 的预编译包把这些依赖都打包好了,装完就能用。
如果你坚持用 pip,那至少要确保系统里有对应的 C++ 运行库。Windows 上常见的c++2005之类报错,本质就是运行库缺失,跟 CadQuery 本身没关系,装个完整的运行库合集基本能解决。
注意:环境隔离一定要做。CadQuery 对 Python 版本和依赖版本比较敏感,混在系统环境里很容易和别的库打架。用虚拟环境,出问题直接删了重建,成本最低。
3.2 定义你的文本描述 schema
前面说了 schema 的重要性,这里给一个能直接用的最小版本。我把它设计成"基础体 + 特征列表"的结构,覆盖大部分简单零件:
# schema_example.py PART_SCHEMA = { "name": "part_name", "base": { "type": "box | cylinder | sphere", "params": {} # 各类型对应不同参数 }, "features": [ # 每个特征是一个 dict ], "unit": "mm" } # 圆柱基础体参数 CYLINDER_PARAMS = {"diameter": float, "height": float} # 盒子基础体参数 BOX_PARAMS = {"length": float, "width": float, "height": float}这个 schema 故意做得简单,因为最小可用版本的目标是跑通链路,不是覆盖所有情况。等你跑通了,再往里加倒角、阵列、抽壳这些特征。
3.3 用 CadQuery 把参数变成实体
接下来是核心的构建代码。我写一个函数,吃上面的 schema,吐出 CadQuery 的实体对象:
import cadquery as cq def build_part(spec): base = spec["base"] btype = base["type"] p = base["params"] if btype == "cylinder": solid = cq.Workplane("XY").circle(p["diameter"] / 2).extrude(p["height"]) elif btype == "box": solid = cq.Workplane("XY").box(p["length"], p["width"], p["height"]) else: raise ValueError(f"unsupported base type: {btype}") # 逐个应用特征 for feat in spec.get("features", []): if feat["type"] == "hole" and feat["subtype"] == "through": x, y = feat["position"] solid = ( solid.faces(">Z").workplane() .center(x, y) .hole(feat["diameter"]) ) return solid这段代码的逻辑很直白:先按基础体类型造一个毛坯,再在指定面上打孔。faces(">Z")是选 Z 方向最高的那个面作为工作平面,center(x, y)把工作平面原点移到孔的位置,hole()打一个贯穿孔。
这里有个细节值得说:为什么用faces(">Z")而不是直接指定坐标。因为 CadQuery 是"选择器驱动"的,你描述的是"我要在最高的那个面上操作",而不是"我要在 z=10 的平面上操作"。前者在参数变化时更鲁棒——你把高度从 10 改成 20,代码不用动;后者就得同步改坐标。这是参数化建模思维和传统坐标思维的区别。
3.4 导出 STEP 并验证
最后一步,导出:
def export_step(solid, filename): cq.exporters.export(solid, filename, exportType="STEP") spec = { "name": "flange", "base": {"type": "cylinder", "params": {"diameter": 50, "height": 10}}, "features": [ {"type": "hole", "subtype": "through", "diameter": 20, "position": [0, 0]} ], "unit": "mm" } solid = build_part(spec) export_step(solid, "flange.step")跑完你会得到一个flange.step,用任意 CAD 软件打开,应该是一个直径 50、厚 10、中心有 20 通孔的圆盘。
验证环节千万别省。我见过太多人导出成功就以为完事了,结果打开一看单位错了(变成英寸)、或者孔的位置偏了。每次改完 schema 或构建逻辑,都要实际打开文件量一下关键尺寸。这一步花两分钟,能省掉后面几小时的返工。
4. 从 STEP 到 URDF 和 G-code:下游格式的转换门道
跑通 STEP 只是起点。真实项目里,你生成的几何往往要喂给仿真或加工环节,这就涉及格式转换。这一节讲两个最常见的下游:URDF 和 G-code。
4.1 URDF 不是"带关节的 STEP"
很多人第一次做 URDF 导出时会想:不就是把 STEP 加上关节信息吗?实际做起来会发现完全不是一回事。
URDF 的核心是树状的运动学结构。一个机器人由若干 link(连杆)和 joint(关节)组成,joint 连接两个 link,定义它们之间的相对运动。每个 link 有自己的视觉几何、碰撞几何、惯性参数,每个 joint 有类型(旋转、平移、固定)、轴向量、限位。
从 text-to-cad 的角度,生成 URDF 意味着你的文本描述里得包含结构信息,而不只是形状信息。比如"一个两轮小车,底盘是 200x150x50 的盒子,左右各一个直径 60 的轮子,轮子绕 Y 轴旋转",这里面既有几何,又有拓扑和运动约束。
实操上,我建议分两步走:先用 text-to-cad 生成各个零件的 STEP,再写一个装配脚本把它们按 URDF 的层级组装起来。CadQuery 本身不直接出 URDF,但你可以用它的几何数据配合urdfpy或直接手写 XML 来生成。
# 简化的 URDF link 片段生成 def make_link(name, mesh_file, mass=1.0): return f""" <link name="{name}"> <visual> <geometry><mesh filename="{mesh_file}"/></geometry> </visual> <collision> <geometry><box size="0.2 0.15 0.05"/></geometry> </collision> <inertial> <mass value="{mass}"/> <inertia ixx="0.001" ixy="0" ixz="0" iyy="0.001" iyz="0" izz="0.001"/> </inertial> </link> """注意这里视觉几何用 mesh,碰撞几何用 box。这就是前面说的碰撞体简化原则。仿真引擎做碰撞检测时,用简单形状比用精细网格快几个数量级,而视觉上你又希望看到精细模型,所以两者分开配。
导入仿真环境(比如 CoppeliaSim 这类)时,最常见的坑是坐标系对不上。URDF 里每个 link 的几何是相对自身坐标系的,joint 的 origin 定义了子 link 相对父 link 的位姿。如果这一步的旋转、平移写错,模型要么飞出去,要么关节转起来像抽筋。我的经验是:先在纸上把坐标系画清楚,标好每个 joint 的 origin 和 axis,再写 XML。跳过这步直接写,调试时间会翻倍。
4.2 G-code 生成:几何只是原料,刀路才是成品
G-code 这条链路比 URDF 更"重",因为从几何到可执行指令,中间隔着工艺规划。
以 3D 打印为例,流程是:STEP/STL → 切片 → G-code。切片器(比如常见的开源切片工具)负责把三维模型切成一层层的二维轮廓,再规划每层的打印路径、填充密度、支撑结构。text-to-cad 在这里的角色是提供干净、封闭、法线正确的几何。
这里有个血泪教训:几何不封闭,切片必出问题。如果你的模型有破面、有自相交、有零厚度的地方,切片器要么报错,要么切出乱七八糟的结果。所以 text-to-cad 生成几何后,最好加一道几何健康检查:
def check_solid(solid): # 检查是否是有效实体 if not solid.val().isValid(): raise ValueError("invalid solid") # 检查体积是否为正 vol = solid.val().Volume() if vol <= 0: raise ValueError("non-positive volume") return True对于数控加工(CNC)的 G-code,逻辑又不一样:你需要考虑刀具半径补偿、进给方向、粗精加工分层。这部分通常交给 CAM 软件处理,text-to-cad 负责把精确的 STEP 喂进去就行。
提示:如果你的目标是"文本直接出打印文件",别自己从零写切片器。用成熟切片器的命令行接口,把 text-to-cad 的输出接进去,省时省力还稳定。
5. 实测中那些文档不会告诉你的坑
前面讲的都是"应该怎么做",这一节讲"实际做的时候会怎么翻车"。这些是我和身边同行踩出来的,文档里基本找不到。
5.1 单位问题:一个能让你所有尺寸错 25.4 倍的坑
CAD 领域单位混乱是历史遗留问题。STEP 文件内部有单位定义,但不同软件默认单位不一样。有的默认毫米,有的默认英寸。text-to-cad 生成时如果不显式指定单位,下游打开可能直接放大或缩小 25.4 倍。
我的做法是:在 schema 里强制要求 unit 字段,构建时统一转成毫米,导出时在文件头显式声明单位。CadQuery 默认就是毫米,这点比较省心,但如果你用别的库,一定要确认。
5.2 浮点精度:为什么你的孔看起来是椭圆的
参数化内核用浮点数表示几何,理论上直径 20 的孔就是精确的 20。但当你把模型导出成某些格式、或者经过多次转换后,精度会损失。表现就是圆孔看起来有点扁,或者两个本该重合的面之间有微小缝隙。
对策是尽量减少格式转换次数。能从参数化内核直接导出 STEP 就别先转 STL 再转回来。另外,在比较尺寸时用容差而不是精确相等:
def approx_equal(a, b, tol=1e-6): return abs(a - b) < tol5.3 大模型生成 schema 时的"幻觉参数"
如果你用大模型做自然语言到 schema 的转换,一定会遇到模型编造参数的情况。你描述里没提壁厚,它给你填个 2mm;你没说孔的位置,它默认放中心。这些"合理但未经确认"的默认值,是批量生成时最大的隐患。
我的处理方式是在 schema 里区分"必填"和"可选",必填字段如果模型没提取到,直接报错让人来补,绝不自动填默认值。宁可中断,也不要生成一批"看起来对但实际错"的模型。
5.4 批量生成时的命名和版本管理
当你一次生成几百个模型时,文件命名会变成灾难。part1.step、part2.step这种命名,过两天你自己都不知道哪个是哪个。
建议用内容哈希或参数指纹做文件名,比如把关键参数拼起来做 hash:
import hashlib, json def make_filename(spec): key = json.dumps(spec, sort_keys=True) h = hashlib.md5(key.encode()).hexdigest()[:8] return f"{spec['name']}_{h}.step"这样同样的参数永远生成同样的文件名,改了参数文件名自动变,天然支持去重和版本追溯。
6. 这条链路还能往哪走:几个值得试的扩展方向
跑通基础流程后,你会发现 text-to-cad 的想象空间比想象中大。分享几个我自己试过、觉得有价值的方向。
参数化模板库。把常用零件(法兰、支架、齿轮)做成参数化模板,文本描述只需要填参数。这比每次从零生成靠谱得多,因为模板里的几何逻辑是人工验证过的,大模型只负责填数,出错概率大幅降低。
和现有 CAD 软件联动。很多 CAD 软件支持脚本接口,你可以让 text-to-cad 生成建模脚本,直接在软件里执行。这样既享受了文本驱动的便利,又能用上成熟软件的工程图、装配、渲染能力。中望 CAD、FreeCAD 这类都有 Python 接口,值得研究。
几何校验自动化。在生成和导出之间插一道自动校验,检查壁厚是否过薄、是否有干涉、是否符合加工约束。这一步能把很多问题挡在下游之前。比如最小壁厚检查,可以用几何库算模型上任意两点间的最小距离,低于阈值就报警。
反向链路:从 CAD 到文本。既然能从文本生成 CAD,反过来把现有模型解析成结构化描述也很有用——便于检索、便于批量修改、便于做设计知识库。这个方向目前工具还不成熟,但思路值得关注。
最后说个我自己的体会:text-to-cad 这类工具,最大的价值不是"替代人画图",而是把工程师从重复劳动里解放出来,去做真正需要判断力的部分——工艺可行性、装配合理性、成本权衡。工具越自动化,人的判断力越值钱。把链路搭顺,把校验做扎实,剩下的交给你的专业直觉。