1. 从一段文字到三维模型:text-to-cad 到底在解决什么问题
第一次听到 "text-to-cad" 这个词,很多做机械设计或者工业建模的朋友第一反应是:又来个炒概念的。毕竟 CAD 这行当,从二维图纸到三维实体,每一步都靠鼠标点出来、靠尺寸约束卡出来,你跟我说打一行字就能出模型?但真上手跑过几个开源方案之后,我的看法变了——它不是要取代工程师,而是把"从想法到第一版可编辑几何体"这段最枯燥的路给铺平了。
所谓 text-to-cad,直译就是"文本转 CAD 模型"。你输入一句自然语言描述,比如"一个外径 80mm、内径 40mm、厚度 12mm 的法兰盘,带 6 个均布 M8 螺栓孔",系统输出一个标准格式的三维模型文件,常见的就是STEP、GLB、STL这几种。STEP 是工业界通用的边界表示格式,能保留精确的曲面和实体拓扑,丢进 SolidWorks、中望 CAD、Fusion 这些软件里还能继续改;GLB 是面向渲染和 Web 展示的轻量格式,带材质和层级;STL 则是三角网格,3D 打印切片软件最认它。这三种格式基本覆盖了"后续要编辑""要在线预览""要直接打印"三类需求。
这件事的价值在哪?我举个自己碰到的场景。做非标设备的时候,前期方案阶段经常要快速比选十几个结构件的外形,传统做法是每个都从头拉伸、打孔、倒角,一个下午就没了。而用 text-to-cad 的思路,把常用件写成参数化描述,改几个数字就能批量出 STEP,拿去装配验证干涉,效率完全不是一个量级。它适合的人其实很明确:做机械结构设计的工程师、搞创客和 3D 打印的玩家、需要批量生成标准件的开发者,以及想把设计流程自动化的团队。哪怕你只是刚学 CAD 制图入门,用它来快速生成一个参考模型,也比对着教程一步步画要直观得多。
但这里必须先泼一盆冷水:text-to-cad 目前的能力边界很清楚。它擅长的是规则明确、参数可枚举的零件——法兰、支架、齿轮毛坯、外壳、连接件。你让它生成一个"造型优雅的汽车曲面",它大概率给你一坨没法用的东西。所以正确的定位是:它负责把 0 变成 1,你负责把 1 打磨成 100。想清楚这一点,后面的所有技术选型和实操才不会跑偏。
2. 拆解 text-to-cad 的技术链路:一句话是怎么变成 STEP 文件的
2.1 自然语言到结构化参数的翻译层
整条链路的第一环,是把人话翻译成机器能算的参数。这一步现在主流有两种做法。一种是基于大语言模型的参数抽取:把用户的描述连同预定义的参数 schema 一起喂给模型,让它输出 JSON,比如{"type": "flange", "outer_diameter": 80, "inner_diameter": 40, "thickness": 12, "hole_count": 6, "hole_diameter": 8.5}。另一种是基于模板和正则的规则匹配,适合描述高度固定的批量场景。
我实测下来,纯靠大模型抽参数有两个坑。第一是单位歧义,"厚度 12"到底是毫米还是厘米,模型不会主动问,你得在 prompt 里强制约定"所有尺寸默认毫米"。第二是数值幻觉,你说了外径 80,它可能给你输出 80.0 也可能输出 8,尤其在描述复杂的时候。所以稳妥的做法是:抽完参数后加一层校验和回填,用 Pydantic 之类的工具做类型和范围检查,超出合理区间就报错让人确认,而不是闷头往下走。
from pydantic import BaseModel, Field, validator class FlangeParams(BaseModel): outer_diameter: float = Field(..., gt=0, le=2000) inner_diameter: float = Field(..., ge=0) thickness: float = Field(..., gt=0, le=500) hole_count: int = Field(0, ge=0, le=64) hole_diameter: float = Field(0, ge=0) @validator("inner_diameter") def inner_less_than_outer(cls, v, values): if "outer_diameter" in values and v >= values["outer_diameter"]: raise ValueError("内径必须小于外径") return v这段校验看着简单,但它拦住的是后面几何内核直接崩溃的灾难。我踩过一次,内径比外径还大,几何内核直接抛异常,整个批处理任务挂掉,排查了半天才发现是输入描述里"内孔 90、外圆 80"这种明显笔误没人管。
2.2 参数化几何的构建:为什么选内核比选语言重要
拿到结构化参数后,就要真正造几何了。这一步的核心是几何内核,它决定了你能不能输出精确的 STEP。目前开源圈子里最常用的是OpenCASCADE(OCCT),Python 侧通过pythonocc-core或者cadquery调用。CadQuery 的定位很讨喜——它用链式 API 把建模过程写成代码,可读性接近自然语言,特别适合做 text-to-cad 的中间层。
import cadquery as cq result = ( cq.Workplane("XY") .circle(40) # 外径 80 -> 半径 40 .circle(20) # 内径 40 -> 半径 20 .extrude(12) # 厚度 12 .faces(">Z").workplane() .polarArray(30, 0, 360, 6) # 6 个孔均布 .hole(8.5) # M8 底孔直径约 8.5 ) result.val().exportStep("flange.step")这段代码就是 text-to-cad 的"心脏"。为什么用 CadQuery 而不是直接怼 OCCT 的底层 API?因为底层 API 光是创建一个圆柱就要写十几行,维护成本极高,而 CadQuery 把常用操作封装成了接近工程语言的表达。但要注意,CadQuery 的polarArray参数顺序容易记混,第一个参数是半径、第二个是起始角、第三个是总角度、第四个是数量,写错了孔的位置就全乱。我建议每次生成后都导出 GLB 在浏览器里转一圈看看,肉眼确认比看代码快得多。
2.3 三种输出格式的取舍逻辑
生成完几何,导出格式的选择直接决定了下游能不能用。我把这三种格式的关键差异整理成表,方便对照:
| 格式 | 本质 | 能否保留精确曲面 | 能否继续参数化编辑 | 典型用途 |
|---|---|---|---|---|
| STEP | 边界表示(B-Rep) | 能 | 能(导入后仍是实体) | 工业设计、装配、CNC 加工 |
| GLB | 三角网格 + 材质 | 否(已离散化) | 否 | Web 预览、AR/VR、渲染 |
| STL | 纯三角网格 | 否 | 否 | 3D 打印、切片 |
这里有个很多人踩的坑:STL 转 STEP 是有损且困难的。热搜里经常有人问"sw 中 stl 转 stp",答案基本是——转出来的 STEP 是一堆碎面片,不是真正的实体,没法做布尔运算。所以 text-to-cad 的正确姿势是从参数直接生成 STEP,需要网格时再从 STEP 离散出 STL 或 GLB,而不是反过来。方向搞反了,后面全是坑。
3. 动手搭一个最小可用的 text-to-cad 流程
3.1 环境准备里最容易被忽略的两件事
搭环境这一步,网上教程大多只告诉你pip install cadquery,但真正卡人的是另外两件事。第一是OCCT 的版本匹配,CadQuery 对底层 OCCT 版本有要求,用 conda 装通常比 pip 稳,因为 conda 会把二进制依赖一起解决。第二是中文字体和路径问题,如果你的输出路径或者参数里带中文,某些几何内核的 IO 模块会直接报错,建议工作目录全用英文。
conda create -n t2cad python=3.10 conda activate t2cad conda install -c conda-forge cadquery pip install pydantic openai装完之后先跑一个最小验证,确认内核能正常导出 STEP,别等写完一大套逻辑才发现底层是坏的。
import cadquery as cq box = cq.Workplane("XY").box(10, 10, 10) box.val().exportStep("smoke_test.step") print("内核正常")3.2 把描述文本喂给模型并约束输出
参数抽取这一步,关键在于给模型一个明确的输出契约。我习惯在 system prompt 里把可用零件类型、每个类型的必填参数、单位约定全部列清楚,并要求它只输出 JSON,不要任何解释文字。这样解析起来最省事。
import json from openai import OpenAI client = OpenAI() SYSTEM_PROMPT = """ 你是一个 CAD 参数抽取器。用户会用自然语言描述一个零件。 你只能输出 JSON,不要输出任何其他文字。 所有尺寸单位统一为毫米。 支持的零件类型:flange(法兰)、plate(板件)、bracket(支架)。 flange 参数:outer_diameter, inner_diameter, thickness, hole_count, hole_diameter plate 参数:length, width, thickness, corner_radius bracket 参数:base_length, base_width, height, thickness """ def extract_params(user_text): resp = client.chat.completions.create( model="gpt-4o-mini", messages=[ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": user_text}, ], temperature=0, ) raw = resp.choices[0].message.content.strip() return json.loads(raw)temperature=0是为了让输出尽量稳定,同样的描述每次抽出来的参数一致,方便复现。这一点在批量生成时特别重要,否则你没法解释为什么昨天生成的模型和今天的不一样。
3.3 从参数到几何的分发逻辑
拿到 JSON 之后,需要一个分发器根据type字段调用对应的建模函数。这里的设计要点是每个零件类型一个独立函数,而不是写一个巨大的 if-else 堆在一起。独立函数便于单独测试,也方便后续扩展新类型。
def build_flange(p): return ( cq.Workplane("XY") .circle(p["outer_diameter"] / 2) .circle(p["inner_diameter"] / 2) .extrude(p["thickness"]) .faces(">Z").workplane() .polarArray(p["outer_diameter"] / 2 - 8, 0, 360, p["hole_count"]) .hole(p["hole_diameter"]) ) def build_plate(p): return ( cq.Workplane("XY") .rect(p["length"], p["width"]) .extrude(p["thickness"]) .edges("|Z").fillet(p.get("corner_radius", 0)) ) BUILDERS = {"flange": build_flange, "plate": build_plate}注意polarArray里那个outer_diameter / 2 - 8,这是把螺栓孔布置在离外缘 8mm 的位置,属于工程经验值。如果直接放在外径上,孔会切穿边缘,模型直接废掉。这种"常识性约束"最好写死在建模函数里,而不是指望语言模型每次都记得。
3.4 导出与批量处理
单个零件跑通后,批量处理就是套一层循环。但批量场景下有两个细节必须处理:文件名唯一性和失败隔离。一个零件生成失败不能拖垮整批任务,所以每个零件用 try-except 包起来,失败的记录到日志里单独处理。
import os, traceback def batch_generate(descriptions, out_dir="output"): os.makedirs(out_dir, exist_ok=True) report = [] for i, desc in enumerate(descriptions): try: params = extract_params(desc) builder = BUILDERS[params["type"]] model = builder(params) path = os.path.join(out_dir, f"part_{i:03d}.step") model.val().exportStep(path) report.append({"index": i, "status": "ok", "path": path}) except Exception as e: report.append({"index": i, "status": "fail", "error": str(e)}) return report跑完一批之后,我习惯把 report 里失败的挑出来,看看是描述本身有问题还是抽取出了问题。十有八九是描述太模糊,比如"做个差不多大的板子"——这种没有具体尺寸的输入,任何系统都救不了。
4. 实测中那些文档不会告诉你的坑
4.1 单位、精度与浮点误差的连锁反应
几何内核处理浮点数时,**容差(tolerance)**是个绕不开的东西。OCCT 默认的建模容差在 1e-7 量级,当你输入的尺寸特别小(比如微米级)或者特别大(比如米级)时,布尔运算可能因为容差判断失败而报错。我遇到过一次,生成一个 0.5mm 厚的薄片,倒角半径设了 0.5mm,结果内核认为倒角面退化成一条线,直接抛异常。解决办法是把倒角半径限制在厚度的 40% 以内,或者干脆对薄壁件跳过倒角。
另一个高频问题是孔位与边界的干涉。前面提到的螺栓孔位置,如果hole_count很大而外径很小,相邻两个孔会重叠,布尔减运算后得到一个奇怪的连体孔。稳妥的做法是在建模前做一次几何可行性检查:孔中心所在圆周的周长除以孔数,必须大于孔径加一个安全余量。
import math def check_hole_feasibility(outer_d, hole_count, hole_d, margin=2.0): if hole_count <= 1: return True pitch_circle = outer_d - 16 # 假设孔心圆比外径小 16 circumference = math.pi * pitch_circle spacing = circumference / hole_count return spacing >= hole_d + margin这个检查不通过就直接拒绝生成并提示用户,比生成一个废模型再让人去 CAD 里修要友好得多。
4.2 语言模型对工程语义的误解
语言模型在工程语境下有个很隐蔽的问题:它会把工艺描述当成几何描述。比如你说"这个面要铣削加工",它可能理解成要在这个面上加一个凹槽。再比如"倒角 2×45°",它可能只抽出一个数字 2,把角度丢了。我的应对办法是在 prompt 里明确区分"几何参数"和"工艺要求",并告诉模型工艺要求一律忽略,只抽几何。
还有一个更麻烦的:相对尺寸。用户说"孔比外径小一圈",这个"一圈"是多少?模型会瞎猜。所以我在 prompt 里加了一条硬规则——遇到相对描述,必须要求用户给出绝对值,否则拒绝生成。宁可多问一句,也不要生成一个尺寸全错的模型。
4.3 导出 STEP 后在其他 CAD 里打不开
这个问题我被问过太多次。STEP 导出后在 SolidWorks 或者中望 CAD 里打不开,通常有三个原因。第一是版本兼容,STEP 有 AP203、AP214、AP242 等不同协议,老软件可能只认 AP203,导出时指定协议能解决大部分问题。第二是模型本身有自相交或非流形边,这种模型在 CadQuery 里能导出,但严格的内核会拒绝加载。第三是文件编码或路径问题,前面提过,中文路径是重灾区。
# 指定 STEP 协议版本,提升兼容性 from OCP.STEPControl import STEPControl_Writer, STEPControl_AsIs from OCP.Interface import Interface_Static Interface_Static.SetCVal("write.step.schema", "AP214")实测下来,导出 AP214 的兼容性最好,绝大多数主流 CAD 都能正常读取。如果对方用的是很老的版本,再退到 AP203。
4.4 批量生成时的内存与性能
CadQuery 每生成一个模型都会在内存里保留几何对象,批量跑几百个之后内存会明显上涨。我的做法是每生成一个就显式释放,并且把导出和建模分开——建模完立刻导出,导出完立刻丢弃对象引用。
import gc for desc in descriptions: model = build_one(desc) model.val().exportStep(path) del model gc.collect()另外,几何内核的布尔运算是单线程的,想提速只能靠多进程。但多进程下每个进程都要独立初始化内核,启动开销不小,所以批量规模小于 50 个的时候,多进程反而更慢。这个临界点因机器而异,建议自己压测一下再决定。
5. 这套东西能落到哪些真实场景里
5.1 标准件与非标件的快速出图
机械设计里大量时间花在重复性的标准件建模上。螺栓、垫片、法兰、轴承座,这些零件参数固定、形状规则,正是 text-to-cad 的甜区。把常用件做成描述模板,需要的时候改几个数字,几秒钟出 STEP,直接拖进装配体。我自己的做法是维护一个描述库,每个零件一行文本,配合一个批量脚本,需要哪批就生成哪批。
对于非标件,它的价值在于方案阶段的快速迭代。客户说"这个支架再高一点、底板再宽一点",传统做法是打开 CAD 改草图、重建、重新装配。用参数化描述,改两个数字重新生成,装配关系如果用的是坐标定位,甚至不用重做。这个效率差距在方案反复修改的项目里体现得淋漓尽致。
5.2 3D 打印前的模型准备
玩 3D 打印的朋友对 STL 肯定不陌生。text-to-cad 在这里的用法是:先用参数生成精确的 STEP,确认尺寸无误后,再离散成高精度的 STL 去切片。为什么不直接生成 STL?因为 STL 是网格,改一个尺寸就得重新离散,而 STEP 改参数重新导出即可。而且从 STEP 离散时你可以控制网格精度,打印出来的表面质量更可控。
这里有个经验:STL 的网格精度不是越高越好。精度太高文件巨大,切片软件卡顿;精度太低曲面变成多边形。一般曲面零件用 0.05mm 的弦高容差,平面零件用 0.1mm 就够了。这个参数在 CadQuery 导出 STL 时可以指定。
from cadquery import exporters exporters.export(model, "part.stl", tolerance=0.05, angularTolerance=0.1)5.3 与现有 CAD 工作流的衔接
很多人担心 text-to-cad 生成的东西和现有工作流脱节。其实 STEP 作为中间格式,衔接得很自然。生成的 STEP 导入 SolidWorks 后是实体,可以继续加特征、做装配、出工程图。导入中望 CAD 也没问题,国产软件对 STEP 的支持这些年做得很到位。唯一要注意的是坐标系约定,CadQuery 默认 Z 轴向上,而有些 CAD 习惯 Y 轴向上,导入后可能需要旋转一下。这个在导出前统一处理掉,比导入后再转要省事。
如果你的团队用 Python 做设计自动化,那 text-to-cad 更是天然契合。把参数抽取、建模、导出、校验串成一条流水线,接上 PLM 或者 ERP 系统,就能实现"下单即出图"的自动化。当然,这条链路要上生产,还得加上权限、版本、审核这些工程化的东西,但技术底座就是前面讲的这些。
6. 几个我反复验证过的实操心得
先说参数校验这件事。我一开始觉得有语言模型兜底,输入随便点没关系,结果被现实教育了好几次。后来我把校验做成了三层:第一层是 JSON schema 校验,保证字段齐全、类型正确;第二层是几何可行性校验,比如内径小于外径、孔不重叠、倒角不超过壁厚;第三层是生成后抽检,随机挑几个模型导出 GLB 在浏览器里看一眼。三层下来,废品率从最初的三成降到了几乎为零。
再说 prompt 的写法。我试过很多版本,最后发现最有效的不是把 prompt 写得多长,而是给几个高质量的示例。在 system prompt 里放两三个"描述到 JSON"的完整例子,模型抽参数的准确率明显提升,尤其是对"均布""对称""带圆角"这类工程词汇的理解。示例比规则管用,这是我在实际调优里最实在的体会。
关于格式选择,我的默认策略是永远先生成 STEP。STEP 是母版,GLB 和 STL 都是派生物。需要预览就现从 STEP 转 GLB,需要打印就现转 STL。这样无论下游要什么,你手里始终有一个精确的、可编辑的源头。反过来,如果你只存了 STL,想改尺寸的时候就得从头再来,这个教训我吃过不止一次。
最后提一个容易被忽视的点:版本管理。text-to-cad 生成的文件往往批量产生,如果不做命名规范和版本记录,很快就会乱成一锅粥。我的做法是文件名里带上零件类型、关键尺寸和时间戳,比如flange_od80_id40_t12_20250101.step,一眼就能看出是什么。配合一个简单的 CSV 台账记录每次生成的参数和文件路径,回溯的时候非常方便。这些看着是小事,但真到了要复现某个历史版本的时候,你会感谢当初多写的这几个字段。