ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

text-to-cad 实战:Python 参数化建模与 STEP/URDF 导出全链路

text-to-cad 实战:Python 参数化建模与 STEP/URDF 导出全链路

1. 从一段文字到三维实体:text-to-cad 到底在解决什么问题

第一次听到 "text-to-cad" 这个词,很多人会下意识地把它理解成"用嘴画图"——说一句话,软件自动帮你把零件建出来。这个理解方向没错,但真正落地的时候,它解决的其实是一个更具体、更痛的问题:设计意图到几何模型之间的那道手工翻译鸿沟。

做过机械设计或者参数化建模的人都清楚,一个再简单的法兰盘,从"我要一个外径 80、内径 40、厚度 10、带 6 个均布 M6 孔的圆盘"这句话,到最终能在 CAD 里旋转、装配、出工程图的实体,中间要经历草图、约束、拉伸、阵列、打孔、倒角这一长串操作。熟练工可能三五分钟搞定,但如果是几十上百个规格的系列件,或者需要频繁根据参数调整的迭代设计,这种重复劳动就会变成纯粹的消耗。

text-to-cad 的核心价值,就是把这句自然语言描述,直接翻译成机器可读、可编辑、可继续加工的三维几何数据。它不是一个孤立的玩具,而是一条链路上的关键一环:自然语言 → 结构化参数 → 几何内核指令 → 标准格式文件(STEP/URDF 等)→ 下游仿真或制造。

这条链路里,Python 几乎是默认的胶水语言。原因很直接:几何内核(比如 OpenCASCADE)有成熟的 Python 绑定,参数解析、单位换算、批量生成、格式导出这些脏活累活,用 Python 写起来最快。而 STEP 作为中性几何交换格式,URDF 作为机器人描述格式,分别对应了"给制造"和"给仿真"两个最主要的出口。

所以这篇内容适合谁看?如果你是会一点 Python、对 CAD 有基本概念、想让建模流程自动化的人,那接下来的拆解会非常对味。如果你是完全零基础,也没关系,我会把每个环节的"为什么"讲清楚,你照着搭一遍就能跑通。整篇内容围绕 text-to-cad 这条主线,把参数解析、几何生成、格式导出、踩坑经验全部串起来,尽量做到看完就能动手。

2. 拆解 text-to-cad 的技术链路:从一句话到可用的几何文件

2.1 自然语言到结构化参数:别急着上大模型

很多人一上来就想用大语言模型直接输出 CAD 代码,这个思路在 demo 阶段很爽,但真到工程场景里,稳定性是最大的敌人。我的建议是分两层处理:先用规则或轻量解析把自然语言拆成结构化参数字典,再让参数字典去驱动几何生成。这样即使解析层出错,你也能一眼看出是哪个字段的问题,而不是面对一大段生成的代码抓瞎。

举个实际例子。输入是"外径 80、内径 40、厚度 10、6 个均布 M6 孔的圆盘"。你要做的第一件事是抽取出:

params = { "outer_diameter": 80.0, "inner_diameter": 40.0, "thickness": 10.0, "hole_count": 6, "hole_diameter": 6.0, # M6 通孔按 6.0 处理,实际可查表 "unit": "mm" }

这里有个容易被忽略的细节:M6 是螺纹规格,不等于孔径。M6 的底孔直径在钢件上通常是 5.0mm 左右,通孔一般取 6.6mm。如果你直接把 6.0 当成孔径,装配的时候螺栓根本穿不过去。所以参数解析层最好内置一张螺纹规格表,把"M6"映射成合理的通孔直径。这个坑我在第一次做批量法兰生成时踩过,生成了一百多个件,结果全部装不上,返工重来。

单位也是重灾区。自然语言里"80"到底是毫米还是厘米?工程语境下默认毫米,但如果用户输入"8 厘米",你就得做换算。我的做法是:默认单位设为毫米,一旦检测到"厘米/cm/米/m/英寸/inch"等词,立刻做归一化,并且在最终输出前打印一次单位确认,避免静默错误。

2.2 几何内核的选择:为什么 OpenCASCADE 是绕不开的选项

参数有了,接下来要把它变成真正的三维实体。这一步必须依赖几何内核,而开源世界里,OpenCASCADE(简称 OCCT)是事实标准。它提供了 B-Rep(边界表示)建模能力,能处理布尔运算、倒角、抽壳这些 CAD 的核心操作,而且有 Python 绑定(pythonocc-core 或者 cadquery 封装)。

为什么不用简单的网格建模(比如直接用 trimesh 拼三角形)?因为网格模型没有精确的曲面定义,你没法在上面做精确的孔位约束,也没法导出成 STEP 这种给制造用的格式。STEP 文件描述的是精确几何,不是三角面片,这是它和 STL 的本质区别。所以只要你的下游是加工或者专业 CAD 软件,就必须走 B-Rep 这条路。

用 CadQuery 举例,生成上面那个法兰盘的核心代码大概是这样:

import cadquery as cq result = ( cq.Workplane("XY") .circle(params["outer_diameter"] / 2) .circle(params["inner_diameter"] / 2) .extrude(params["thickness"]) .faces(">Z") .workplane() .polarArray( radius=(params["outer_diameter"] + params["inner_diameter"]) / 4, startAngle=0, angle=360, count=params["hole_count"] ) .hole(params["hole_diameter"]) ) cq.exporters.export(result, "flange.step")

这段代码里,polarArray的半径取的是内外径的平均值再除以二,也就是孔分布在内外径中间的位置。这个位置不是随便定的——如果孔太靠外,会削弱外缘强度;太靠内,又可能和内孔干涉。工程上一般取内外径的中间值,或者根据螺栓头直径反推。这些"经验值"就是 text-to-cad 里最值钱的部分,纯靠大模型是猜不出来的,必须由懂工艺的人写进规则里。

2.3 STEP 与 URDF:两个出口,两种用途

生成完几何,导出格式的选择直接决定了这个模型能干什么。

STEP是给制造和 CAD 互操作用的。它保留了精确的 B-Rep 几何,能被 SolidWorks、中望 CAD、FreeCAD 等软件打开,也能直接送去做 CAM 编程。如果你要做的是零件加工,STEP 是首选。

URDF则是给机器人仿真用的。它描述的是刚体之间的连接关系(关节类型、运动轴、惯性参数),几何只是其中一部分。当你把 text-to-cad 用在机器人领域时,比如"生成一个两连杆机械臂",输出的就不只是两个圆柱体,还要包含关节的旋转轴定义、连杆的质心位置。URDF 导入 CoppeliaSim 或者 RViz 之后,才能正确地做运动学仿真。

这里有个实操层面的坑:URDF 里的几何通常用简化的碰撞体,而不是精细的视觉模型。如果你把 STEP 里带倒角、带螺纹的精细模型直接塞进 URDF 当碰撞体,仿真会慢到怀疑人生。正确做法是视觉模型用精细网格,碰撞模型用简化几何(比如用圆柱代替带孔的连杆)。这个分离策略在做机械臂仿真时几乎是必须的。

输出格式核心用途几何类型典型下游工具
STEP制造、CAD 互操作精确 B-RepSolidWorks、中望 CAD、FreeCAD
URDF机器人仿真刚体 + 关节 + 简化碰撞体CoppeliaSim、RViz、Gazebo
STL3D 打印、快速预览三角网格切片软件、网格查看器

3. 用 Python 把整条链路串起来:环境、依赖与核心代码

3.1 环境搭建:避开 Python 版本和依赖的连环坑

Python 环境这块,我踩过的坑足够写一篇避坑指南。首先,CadQuery 和 pythonocc-core 对 Python 版本有明确要求,太新的版本(比如刚发布的 3.13)往往没有预编译的 wheel,你得自己编译 OCCT,那个过程能让人崩溃。稳妥的选择是 Python 3.8 到 3.11 之间,我个人长期用 3.10,兼容性最好。

安装 CadQuery 最省事的方式是用 conda,因为它能自动处理 OCCT 这个重量级依赖:

conda create -n text2cad python=3.10 conda activate text2cad conda install -c conda-forge cadquery

如果你坚持用 pip,那就要做好心理准备:pip install cadquery在某些平台上会尝试从源码编译,耗时且容易失败。这时候可以退而求其次,用cadquery-ocp的预编译包,或者直接用 FreeCAD 自带的 Python 环境,它已经内置了 OCCT。

还有一个高频问题:numpy 版本冲突。CadQuery 依赖的某些库对 numpy 版本敏感,如果你在同一个环境里又装了 opencv(cv2),很容易出现 numpy 版本打架。我的做法是给 text-to-cad 单独建一个干净环境,不要和图像处理、量化交易那些库混在一起。环境隔离这件事,前期多花五分钟,后期省下五小时。

3.2 参数校验:在生成几何之前拦住错误

几何生成是计算密集操作,一旦参数有问题,轻则报错,重则生成一个看起来正常但实际尺寸离谱的模型。所以在调用几何内核之前,必须做一轮参数校验。这一步很多人会跳过,但它能挡掉 80% 的低级错误。

校验清单大致包括:

  • 尺寸是否为正数(外径、厚度不能是负数或零)
  • 内径是否小于外径(否则圆环逻辑不成立)
  • 孔的数量是否为整数且大于零
  • 孔分布半径是否落在内外径之间(避免孔打到实体外面)
  • 单位是否已归一化
def validate_params(p): assert p["outer_diameter"] > 0, "外径必须为正" assert 0 < p["inner_diameter"] < p["outer_diameter"], "内径必须小于外径" assert isinstance(p["hole_count"], int) and p["hole_count"] > 0, "孔数必须为正整数" hole_radius = (p["outer_diameter"] + p["inner_diameter"]) / 4 assert p["inner_diameter"] / 2 < hole_radius < p["outer_diameter"] / 2, "孔分布半径越界" return True

这段代码看起来简单,但它救过我很多次。有一次批量生成时,输入表格里有个内径填成了 0,如果没有校验,几何内核会直接抛一个很难懂的底层异常,排查半天才发现是数据问题。把校验做在前面,错误信息就是人话,而不是 OCCT 的天书。

3.3 批量生成与命名规范:别让文件管理拖后腿

单个模型生成只是开始,text-to-cad 真正的威力在批量。假设你有一张 Excel 表,里面是几十个规格的法兰,你要一次性全部生成 STEP 文件。这时候文件命名就成了关键。

我的命名规范是:{类型}_{外径}x{内径}x{厚度}_{孔数}H_{版本}.step,比如flange_80x40x10_6H_v1.step。这样光看文件名就知道规格,不用打开文件。千万不要用part1.step、part2.step这种命名,过两天你自己都不知道哪个是哪个。

批量生成的骨架大概是这样:

import pandas as pd df = pd.read_excel("specs.xlsx") for idx, row in df.iterrows(): params = { "outer_diameter": float(row["外径"]), "inner_diameter": float(row["内径"]), "thickness": float(row["厚度"]), "hole_count": int(row["孔数"]), "hole_diameter": float(row["孔径"]), "unit": "mm" } validate_params(params) model = build_flange(params) fname = f"flange_{params['outer_diameter']}x{params['inner_diameter']}x{params['thickness']}_{params['hole_count']}H_v1.step" cq.exporters.export(model, fname) print(f"已生成 {fname}")

跑完这一轮,几十个 STEP 文件就整整齐齐躺在文件夹里了。这个过程如果手工做,一个件三分钟,几十个就是两三个小时,而且容易出错。自动化之后,几十秒搞定,还能保证规格一致性。

4. 实测中那些文档不会告诉你的坑

4.1 布尔运算失败:几何内核的"玄学"时刻

做 CAD 自动化,布尔运算失败是绕不过去的坎。你明明参数都对,但内核就是告诉你"运算失败"或者生成一个空实体。这种情况在孔刚好和外缘相切、或者两个面完全共面时特别容易发生。

根本原因是浮点精度。几何内核在做布尔运算时,需要判断两个面是否相交、是否共面,这些判断依赖浮点比较。当两个面理论上相切但实际差了一个极小量时,内核就懵了。

我的应对策略有三条:

  1. 留安全余量:孔分布半径不要贴着内外径,留至少 1mm 的余量。上面校验里那个"孔分布半径越界"检查,其实也是在防这个。
  2. 避免完全共面:如果两个实体要贴合,让它们稍微重叠一点点(比如 0.01mm),比完全共面更稳。
  3. 失败重试:捕获异常后,微调参数(比如把孔位旋转 0.1 度)再试一次,往往就过了。

提示:布尔运算失败时,先别怀疑代码逻辑,八成是几何本身太"临界"。把参数往安全区挪一挪,问题通常就消失了。

4.2 单位与坐标系的隐形陷阱

单位问题前面提过,但坐标系的问题更隐蔽。CadQuery 默认在 XY 平面建模,Z 轴向上。但如果你生成的模型要导入到某个特定软件,它的坐标系约定可能不一样。比如有些机器人仿真环境默认 Z 轴向前,有些 CAD 软件默认 Y 轴向上。

坐标系不统一,模型导入后就是躺着的、歪着的。这个坑在跨软件协作时几乎必踩。解决办法是在导出前明确做一次坐标变换,并且在文档里写清楚"本模型采用 Z 轴向上,单位毫米"。别指望下游软件自动猜对你的意图。

4.3 大模型生成代码的可靠性边界

回到 text-to-cad 里"text"这一端。如果你真的想用大语言模型来生成 CadQuery 代码,那必须清楚它的能力边界。模型能写出语法正确的代码,但它不懂工艺。它可能给你生成一个孔位在实体外面的法兰,也可能把 M6 当成 6mm 孔径。所以正确的用法是:让模型负责"翻译"和"补全",让规则和校验负责"兜底"。

我现在的做法是:模型输出结构化参数(JSON),而不是直接输出几何代码。参数经过校验层之后,再由我写死的、经过验证的几何函数去生成模型。这样既享受了自然语言输入的便利,又保证了输出的可靠性。这个架构上的取舍,是踩了无数次坑之后才定下来的。

常见坑表现根因应对
布尔运算失败报错或空实体浮点精度、面共面留余量、避免共面、失败重试
单位错误模型尺寸差 10 倍单位未归一化默认毫米、检测单位词、输出前确认
坐标系不符模型躺倒或旋转各软件约定不同导出前统一变换、文档注明
螺纹孔径错误螺栓装不上M 规格≠孔径内置螺纹规格表
依赖冲突导入即报错numpy 等版本打架独立环境、conda 管理

5. 从能跑到好用:让 text-to-cad 真正进入工作流

5.1 参数化模板库的积累思路

text-to-cad 做久了,你会发现真正有价值的不是那套解析代码,而是积累下来的参数化模板库。法兰、齿轮、支架、连杆、外壳,每一种都有它的参数维度和工艺约束。把这些模板沉淀成函数,下次遇到类似需求,改几个参数就能出图。

我的模板库组织方式是按"零件族"分类,每个族一个 Python 模块,里面包含参数定义、校验规则、几何生成函数、默认导出配置。比如flange.py、gear.py、bracket.py。这样新增一个零件族,就是新增一个模块,互不干扰。

模板库的另一个好处是知识固化。老师傅脑子里的"孔要留多少边距""壁厚不能低于多少",一旦写进校验规则,就变成了团队资产,不会因为人员流动而丢失。这也是 text-to-cad 相比纯手工建模的深层价值——它把隐性经验变成了显性代码。

5.2 与下游工具的衔接:导出之后的事

模型生成出来只是第一步,真正的工作流还要考虑下游。如果你做的是机器人方向,URDF 导出后要导入 CoppeliaSim 做仿真,那就要注意 URDF 里的 mesh 路径、惯性矩阵、关节限位这些字段。惯性矩阵如果随便填,仿真时机械臂会表现得像没有质量一样乱飘。

如果你做的是制造方向,STEP 导出后可能要批量转 PDF 出图,或者导入到中望 CAD 里做进一步标注。这时候可以写一个批处理脚本,用 FreeCAD 的命令行模式做格式转换,把 STEP 批量转成 PDF 或者 DXF。把导出之后的环节也自动化,整条链路才算闭环。

5.3 性能与规模:当零件数量上千时

小批量的时候,怎么实现都行。但当你要生成上千个零件时,性能就成了问题。几何内核的布尔运算是单线程的,一个复杂零件可能要几秒。上千个就是几十分钟。

优化方向有几个:一是并行化,用 Python 的 multiprocessing 把不同零件分到多个进程,几何内核本身不占满多核,并行能线性提速;二是简化几何,非关键特征(比如装饰性倒角)在批量生成时可以省略;三是缓存中间结果,如果多个零件共享基础几何,可以先生成基础体再派生。

from multiprocessing import Pool def generate_one(params): validate_params(params) model = build_flange(params) fname = make_filename(params) cq.exporters.export(model, fname) return fname with Pool(processes=8) as pool: results = pool.map(generate_one, all_params)

这段代码在 8 核机器上,生成速度大概能提升 6 到 7 倍。注意几何内核的进程安全问题,每个进程要独立初始化,不要共享内核对象。

5.4 一个完整的实战案例复盘

最后用一个真实场景收尾。之前有个需求:根据一张包含两百多个规格的表格,批量生成法兰 STEP 文件,并且每个文件要按规格命名,最后打包交付。

我的处理流程是:先读 Excel,做单位归一化和参数校验,把不合格的行单独记下来;然后用多进程批量生成,每个进程独立跑几何内核;生成完做一轮抽检,随机打开几个文件确认尺寸;最后用脚本打包压缩。

整个过程里,最耗时的不是生成,而是前期把表格里的脏数据清理干净。两百多行里,有十几行单位不统一,有几行内径大于外径,还有几行孔数是小数。如果这些不提前处理,生成阶段会各种报错,而且报错信息很难定位到具体是哪一行。所以我现在做任何批量任务,第一步永远是数据清洗和校验,这一步做扎实了,后面就是顺水推舟。

这个案例让我更确信一件事:text-to-cad 的难点从来不在"生成几何"这个动作本身,而在于把模糊的、不规范的输入,变成精确的、可校验的参数。几何内核是现成的,Python 是现成的,真正需要你投入的,是那套把业务语言翻译成工程参数的规则体系。这套体系建好了,text-to-cad 才真正从玩具变成了生产力工具。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进