☰
Python实现text-to-cad:从文本描述到STEP/URDF参数化建模
2026/10/27 14:37:53 网站建设 项目流程

1. 从一句话到三维模型:text-to-cad 到底在解决什么问题

第一次听到 “text-to-cad” 这个词,很多人脑子里冒出来的画面大概是:对着电脑敲一句“给我画一个法兰盘”,然后屏幕上就自动出现一个可以旋转、可以导出、可以拿去加工的 3D 模型。这个画面听起来像是科幻,但它背后其实是一条已经能跑通的技术链路——用自然语言描述几何意图,由程序生成参数化的 CAD 模型,再导出成 STEP、URDF 这类下游工具能直接消费的格式。

我最早接触这个方向,是因为手头有一批重复性极高的建模任务:几十个规格相近但尺寸不同的支架、连接件、安装板。每改一次尺寸就要在图形界面里点半天,改完还要重新导出、重新检查。后来我意识到,这类“结构固定、参数变化”的零件,本质上就是一段可以被文本描述驱动的几何逻辑。text-to-cad 的价值就在这里:它把“描述”和“建模”之间的那层手工操作抽掉,让文本成为几何的输入源。

需要先泼一盆冷水:text-to-cad 不是“AI 帮你从零画任意复杂曲面”的魔法。它真正擅长的是参数化、规则化、可被结构化描述的零件。一个带倒角、带孔阵列、带螺纹的机械件,只要你能用文字把它的尺寸关系和拓扑结构说清楚,它就能被稳定地生成出来。反过来,一个雕塑、一个有机曲面、一个艺术造型,用文本驱动反而比手工建模更费劲。搞清楚这个边界,后面的所有工作才不会跑偏。

这篇文章面向三类人:一是做机械设计、想把手头重复建模自动化的人;二是做机器人仿真、需要批量生成 URDF 模型的人;三是写 Python、想找一个能落地的 CAD 自动化练手项目的人。不管你之前有没有碰过 CAD 二次开发,只要你会写一点 Python、能看懂基本的几何概念,这篇内容都能让你把 text-to-cad 这条链路跑起来。

2. 整体设计思路:为什么是 Python + 参数化建模 + STEP/URDF 导出

2.1 核心链路的四个环节

把 text-to-cad 拆开看,它其实是一条四段式的流水线:

  1. 文本解析:把“直径 50、厚 10、中心一个直径 20 的通孔”这类描述,解析成结构化的参数字典。
  2. 几何构建:用参数化建模库,根据参数字典生成实体几何。
  3. 格式导出:把几何体导出成 STEP(通用交换格式)或 URDF(机器人描述格式)。
  4. 校验与迭代:检查模型是否闭合、尺寸是否正确、能否被下游工具正常读取。

这四个环节里,真正决定项目成败的是第二和第三环节。文本解析可以用正则、可以用大模型、也可以直接手写配置,但几何构建和导出必须稳定、可复现、可批量。这也是为什么我强烈建议用 Python 来做这件事——它的几何生态足够成熟,脚本化能力足够强,而且和下游的仿真、分析工具衔接顺畅。

2.2 为什么选参数化建模而不是直接操作网格

很多人第一反应是用网格(mesh)来做,比如生成 STL。但网格有个致命问题:它是“死”的。你改一个孔的直径,整个网格的拓扑可能都要重算,而且网格没有特征历史,没法追溯“这个面是哪个操作生成的”。

参数化建模(B-rep,边界表示)不一样。它保留了几何的特征树:拉伸、切除、倒角、阵列,每一步都是一个可编辑的节点。你改一个参数,整棵树重新求值,模型自动更新。这对 text-to-cad 至关重要——文本描述的本质就是参数,参数化建模天然就是为参数服务的。

在 Python 生态里,能同时满足“参数化建模”和“STEP 导出”的库并不多。我实测下来比较稳的是CadQuery(基于 OpenCASCADE)和build123d。CadQuery 的链式 API 写起来很顺手,build123d 的语法更接近现代 Python,两者都能导出 STEP。选哪个取决于你的团队习惯,我下面以 CadQuery 为主线来写,因为它的社区资料相对更全,遇到问题更容易搜到答案。

2.3 STEP 和 URDF 的分工

这两个格式经常被混在一起问,其实它们服务的是完全不同的场景。

格式用途关键特点典型下游
STEP通用三维交换保留 B-rep 精确几何,可被几乎所有 CAD 软件读取机械加工、装配、CAM
URDF机器人描述描述连杆、关节、惯性、碰撞体,是 XML 文本机器人仿真、运动学

简单说,STEP 是“给制造看的”,URDF 是“给仿真看的”。一个 text-to-cad 项目如果只导出 STEP,那它服务的是机械设计流程;如果还要导出 URDF,那它服务的是机器人流程。两者可以共用同一套几何构建代码,只是在导出阶段走不同的分支。这一点在设计架构时就要想清楚,否则后期改起来很痛苦。

2.4 文本解析层的选型考量

文本解析这块,我踩过的坑最多。早期我试过用大模型直接输出 CadQuery 代码,结果非常不稳定:同样的描述,十次里有三次生成的代码跑不通,还有两次尺寸理解错了。后来我改成“大模型只负责抽取参数,几何构建用固定模板”,稳定性立刻上来了。

具体做法是:定义一个参数 schema,比如{outer_diameter, thickness, hole_diameter, hole_count},让文本解析层只负责把自然语言映射到这个 schema 上。几何构建层拿到 schema 后,用固定的函数去生成模型。这样即使解析层偶尔出错,错误也是“参数错了”,而不是“代码崩了”,排查起来容易得多。

提示:如果你的描述文本格式比较固定,比如来自 Excel 表格或表单,完全可以跳过自然语言解析,直接用结构化数据驱动。text-to-cad 的核心价值在“文本到几何”的自动化,不在于“必须用自然语言”。

3. 核心细节解析:几何构建、参数计算与导出要点

3.1 环境搭建:Python 与 CadQuery 的安装

先把地基打好。CadQuery 依赖 OpenCASCADE,安装时最容易出问题的就是这一步。我的建议是直接用 conda 装,比 pip 省心很多:

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

如果你坚持用 pip,那就先装cadquery,它会自动拉取OCP这个 OpenCASCADE 的 Python 绑定。但 pip 在 Windows 上偶尔会遇到编译问题,尤其是 Python 版本太新的时候。我实测 Python 3.10 和 3.11 最稳,3.12 早期版本有过兼容问题。

装完之后跑一句验证:

import cadquery as cq result = cq.Workplane("XY").box(10, 10, 10) print(result.val().Volume())

能打印出 1000 就说明环境没问题。如果报ImportError,八成是 OCP 没装好,回去用 conda 重装。

3.2 参数 schema 的设计原则

参数 schema 是整个项目的“接口契约”,设计得好,后面一路顺畅;设计得烂,后面到处打补丁。我的经验是遵循三条原则:

第一,参数名要语义化。不要用d1、d2这种,要用outer_diameter、hole_diameter。文本解析层和几何构建层是两拨人(或者两个模块)在对接,名字含糊就是给自己挖坑。

第二,单位要统一。CAD 里最常见的 bug 就是毫米和米混用。我建议内部一律用毫米,导出 URDF 时再统一除以 1000 转成米,因为 URDF 的标准单位是米。这个转换只在一个地方做,不要散落在各处。

第三,必填和可选要分清。比如一个法兰盘,外径、厚度、孔径是必填的,倒角半径、孔的数量可以是可选的,给默认值。这样文本描述可以很简短,也可以很详细,都能跑。

一个典型的 schema 长这样:

from pydantic import BaseModel, Field class FlangeParams(BaseModel): outer_diameter: float = Field(..., gt=0, description="外径,单位mm") thickness: float = Field(..., gt=0, description="厚度,单位mm") hole_diameter: float = Field(..., gt=0, description="中心孔直径,单位mm") hole_count: int = Field(4, ge=0, description="螺栓孔数量") bolt_circle_diameter: float = Field(0, ge=0, description="螺栓孔分布圆直径") fillet_radius: float = Field(0, ge=0, description="倒角半径")

用 pydantic 的好处是自带校验,参数不合法直接报错,不会带着错误数据往下跑。

3.3 几何构建的关键操作

以法兰盘为例,几何构建的核心步骤是:画外圆、拉伸、切中心孔、切螺栓孔阵列、倒角。用 CadQuery 写出来大概是这样:

import cadquery as cq def build_flange(p: FlangeParams): # 基础圆盘 result = ( cq.Workplane("XY") .circle(p.outer_diameter / 2) .extrude(p.thickness) ) # 中心通孔 result = result.faces(">Z").workplane().hole(p.hole_diameter) # 螺栓孔阵列 if p.hole_count > 0 and p.bolt_circle_diameter > 0: result = ( result.faces(">Z").workplane() .polarArray(p.bolt_circle_diameter / 2, 0, 360, p.hole_count) .hole(p.hole_diameter * 0.5) ) # 倒角 if p.fillet_radius > 0: result = result.edges("|Z").fillet(p.fillet_radius) return result

这段代码里有几个细节值得说。faces(">Z")是选择朝上的那个面作为工作平面,polarArray是极坐标阵列,参数依次是半径、起始角、终止角、数量。倒角那一步用edges("|Z")选中所有平行于 Z 轴的边,这样只倒竖直方向的棱,不会把上下表面的圆边也倒掉。

注意:hole()默认是贯穿整个实体的。如果你只想切一定深度,要用cboreHole或手动cutBlind。我见过有人用hole()想切盲孔,结果把整个零件切穿了,排查了半天。

3.4 参数计算:那些不能拍脑袋的数字

text-to-cad 里有一类参数是“算出来的”,不是“填进去的”。比如螺栓孔分布圆的直径,它和外径、孔径之间有几何约束关系。如果分布圆太大,孔会切到外边缘;太小,孔会切到中心孔。合理的范围是:

hole_diameter/2 + margin < bolt_circle_diameter/2 < outer_diameter/2 - margin

其中 margin 是孔边到边缘的最小壁厚,一般取 1.5 到 2 倍的孔径。这个约束应该在参数校验阶段就检查,而不是等模型生成出来发现破了才回头改。

再比如 URDF 里的惯性参数。很多人直接从 CAD 里读质量,但转动惯量是算出来的。对于一个圆柱体,绕中心轴的转动惯量是I = 0.5 * m * r^2,绕径向轴是I = (1/12) * m * (3r^2 + h^2)。这些公式必须写进导出逻辑里,否则仿真出来的动力学是错的。

参数计算公式说明
圆柱绕轴惯量0.5·m·r²绕中心对称轴
圆柱绕径向惯量(1/12)·m·(3r²+h²)绕垂直于轴的直径
长方体惯量(1/12)·m·(a²+b²)对应轴取另两维

3.5 STEP 导出的坑

STEP 导出看起来就一行代码:

cq.exporters.export(result, "flange.step")

但实际用起来有几个坑。第一,单位。CadQuery 内部是毫米,导出的 STEP 默认也是毫米,但有些下游软件会按米解读,导致模型小了 1000 倍。解决办法是在导出时显式指定单位,或者在文件名里标注。第二,坐标系。CadQuery 默认的 Z 轴朝上,但有些机器人框架默认 Z 轴朝前,导入后模型是躺着的。这个要在导出前用rotate调整好。第三,精度。STEP 有精度设置,默认值对大多数零件够用,但如果你做的是微小特征(比如 0.1mm 的槽),可能需要调高精度,否则导出后特征会丢失。

3.6 URDF 导出的特殊处理

URDF 不是简单的几何导出,它需要把模型拆成“连杆 + 关节”的结构。一个法兰盘如果是一个整体,那它就是一个 link,没有 joint。但如果你的模型是一个机械臂,那每个臂段是一个 link,臂段之间是 joint。

导出 URDF 时,几何部分可以用 STL 或 DAE 作为视觉和碰撞体,惯性部分要手写。一个典型的 link 定义:

<link name="flange"> <visual> <geometry> <mesh filename="flange.stl" scale="0.001 0.001 0.001"/> </geometry> </visual> <collision> <geometry> <mesh filename="flange.stl" scale="0.001 0.001 0.001"/> </geometry> </collision> <inertial> <mass value="0.5"/> <inertia ixx="..." ixy="0" ixz="0" iyy="..." iyz="0" izz="..."/> </inertial> </link>

注意那个scale="0.001 0.001 0.001",这就是毫米转米的处理。如果你导出的 STL 是毫米单位,这里必须缩放,否则导入仿真环境后模型会大得离谱。

4. 实操过程:从文本描述到可用的 STEP 和 URDF

4.1 完整流程的搭建

我把整个流程串成一个可执行的脚本,结构如下:

text2cad/ ├── parser.py # 文本解析 ├── schema.py # 参数定义 ├── builder.py # 几何构建 ├── exporter.py # STEP/URDF 导出 └── main.py # 入口

main.py的逻辑很直白:读文本、解析参数、构建几何、导出文件、打印结果。下面我按顺序讲每个环节的实现。

4.2 文本解析的两种实现

方案一:正则 + 关键词匹配。适合描述格式固定的场景。比如“外径50 厚10 中心孔20”,用正则提取数字和关键词:

import re def parse_flange_text(text): patterns = { "outer_diameter": r"外径\s*(\d+\.?\d*)", "thickness": r"厚\s*(\d+\.?\d*)", "hole_diameter": r"中心孔\s*(\d+\.?\d*)", } params = {} for key, pat in patterns.items(): m = re.search(pat, text) if m: params[key] = float(m.group(1)) return params

这个方案的好处是快、可控、不依赖外部服务。坏处是只能处理你预设过的表达方式,用户换个说法就失效。

方案二:大模型抽取。把文本和 schema 一起发给模型,让它输出 JSON。这个方案灵活,但要注意两点:一是要求模型只输出 JSON,不要加解释;二是拿到 JSON 后必须用 pydantic 校验,不能直接信。

import json from schema import FlangeParams def parse_with_llm(text, llm_client): prompt = f"""从下面的描述中抽取法兰盘参数,输出JSON。 描述:{text} 字段:outer_diameter, thickness, hole_diameter, hole_count, bolt_circle_diameter, fillet_radius 只输出JSON,不要其他内容。""" resp = llm_client.chat(prompt) data = json.loads(resp) return FlangeParams(**data)

我实际用下来,方案一和方案二可以结合:先用正则试,匹配不到再走大模型。这样既快又稳。

4.3 几何构建的完整实现

把 3.3 节的代码补全,加上参数校验和异常处理:

def build_flange(p: FlangeParams): # 校验几何约束 if p.bolt_circle_diameter > 0: inner_limit = p.hole_diameter / 2 + p.hole_diameter * 0.75 outer_limit = p.outer_diameter / 2 - p.hole_diameter * 0.75 if not (inner_limit < p.bolt_circle_diameter / 2 < outer_limit): raise ValueError( f"螺栓孔分布圆 {p.bolt_circle_diameter} 超出合理范围 " f"({inner_limit*2:.1f} ~ {outer_limit*2:.1f})" ) result = cq.Workplane("XY").circle(p.outer_diameter / 2).extrude(p.thickness) result = result.faces(">Z").workplane().hole(p.hole_diameter) if p.hole_count > 0 and p.bolt_circle_diameter > 0: result = ( result.faces(">Z").workplane() .polarArray(p.bolt_circle_diameter / 2, 0, 360, p.hole_count) .hole(p.hole_diameter * 0.5) ) if p.fillet_radius > 0: result = result.edges("|Z").fillet(p.fillet_radius) return result

这里加了一个几何约束校验,这是我在实际项目中养成的习惯。模型生成失败往往不是代码问题,而是参数本身就不合理。提前校验,报错信息清晰,比让 OpenCASCADE 抛一个看不懂的异常强得多。

4.4 STEP 导出的完整流程

def export_step(model, filepath, unit="mm"): cq.exporters.export(model, filepath) print(f"STEP 已导出:{filepath}")

看起来简单,但我在导出后一定会做一件事:重新读回来验证。

def verify_step(filepath): imported = cq.importers.importStep(filepath) solids = imported.solids().vals() if not solids: raise RuntimeError("导出的 STEP 没有实体") vol = sum(s.Volume() for s in solids) print(f"验证通过,实体数 {len(solids)},总体积 {vol:.2f} mm³") return vol

这个验证步骤救过我很多次。有一次导出的模型在 CadQuery 里看着正常,但 STEP 文件里只有一个空壳,原因是某个布尔操作产生了非流形边。重新读回来一验证,立刻暴露。

4.5 URDF 导出的完整流程

URDF 导出分两步:先把几何导出成 STL,再拼装 XML。

def export_urdf_link(model, link_name, mass, filepath): stl_path = filepath.replace(".urdf", ".stl") cq.exporters.export(model, stl_path, exportType="STL") # 计算惯性 bb = model.val().BoundingBox() size = (bb.xlen, bb.ylen, bb.zlen) ixx = mass / 12 * (size[1]**2 + size[2]**2) / 1e6 iyy = mass / 12 * (size[0]**2 + size[2]**2) / 1e6 izz = mass / 12 * (size[0]**2 + size[1]**2) / 1e6 urdf = f"""<?xml version="1.0"?> <robot name="{link_name}"> <link name="{link_name}"> <visual> <geometry><mesh filename="{stl_path}" scale="0.001 0.001 0.001"/></geometry> </visual> <collision> <geometry><mesh filename="{stl_path}" scale="0.001 0.001 0.001"/></geometry> </collision> <inertial> <mass value="{mass}"/> <inertia ixx="{ixx:.6e}" ixy="0" ixz="0" iyy="{iyy:.6e}" iyz="0" izz="{izz:.6e}"/> </inertial> </link> </robot>""" with open(filepath, "w") as f: f.write(urdf)

注意惯性计算里的/ 1e6,这是把毫米平方转成米平方。因为 URDF 的惯性单位是 kg·m²,而我们的尺寸是毫米。这个转换如果漏了,惯性会大 100 万倍,仿真直接飞出去。

4.6 批量生成的实操

text-to-cad 真正的威力在批量。假设你有一个 CSV,每行是一组参数:

import csv from schema import FlangeParams def batch_generate(csv_path, output_dir): with open(csv_path) as f: reader = csv.DictReader(f) for i, row in enumerate(reader): params = FlangeParams(**{k: float(v) for k, v in row.items()}) model = build_flange(params) step_path = f"{output_dir}/flange_{i:03d}.step" export_step(model, step_path) verify_step(step_path)

我跑过一批 200 个零件的生成,全程不到 3 分钟。如果手工建模,一个零件按 5 分钟算,200 个就是 16 个小时。这个效率差距就是 text-to-cad 最直接的价值。

5. 常见问题与排查技巧实录

5.1 安装与环境类问题

问题:ImportError: DLL load failed。这是 Windows 上最常见的问题,原因是 OpenCASCADE 的 DLL 没找到。解决办法是用 conda 装,conda 会自动处理 DLL 路径。如果必须用 pip,确保 Visual C++ Redistributable 装了最新版。

问题:Python 版本太新导致 OCP 装不上。OCP 的预编译包通常滞后于 Python 最新版。我建议锁死 Python 3.10 或 3.11,不要追新。用conda create -n text2cad python=3.11明确指定版本。

问题:conda 装 cadquery 很慢。换国内镜像源,或者用 mamba 替代 conda。mamba 的依赖求解快很多,装 cadquery 这种依赖多的包优势明显。

5.2 几何构建类问题

问题:布尔操作后实体变成空壳。这通常是两个面完全重合导致的。解决办法是在做布尔运算前,把两个实体稍微错开一点,或者用clean()清理一下几何。

问题:倒角失败。倒角半径太大,超过了相邻面的尺寸,就会失败。解决办法是减小倒角半径,或者只对部分边倒角。我一般会加一个 try-except,倒角失败就跳过,不让整个流程崩掉。

问题:孔阵列位置不对。polarArray的起始角是相对于当前工作平面的 X 轴。如果你的工作平面不是默认的,孔的位置会偏。解决办法是显式指定工作平面的方向,或者用transformed调整。

5.3 导出类问题

问题:STEP 导入下游软件后尺寸不对。九成是单位问题。检查导出时是否显式指定了单位,检查下游软件的导入设置。我习惯在文件名里带单位,比如flange_mm.step,减少歧义。

问题:URDF 导入仿真环境后模型巨大或极小。检查 STL 的 scale 属性。如果 STL 是毫米,scale 必须是 0.001。如果 STL 已经是米,scale 就是 1。这个一定要和导出时的单位对齐。

问题:URDF 里惯性参数导致仿真不稳定。检查惯性矩阵是否正定,检查单位是否是 kg·m²。如果惯性太小,仿真会抖;太大,响应会迟钝。用 4.5 节的公式算出来的值一般是对的,但如果你用了简化几何(比如用包围盒代替实际形状),惯性会有偏差。

5.4 常见问题速查表

现象可能原因排查方向
导入报 DLL 错误OpenCASCADE 路径问题改用 conda 安装
模型导出后是空壳布尔运算产生非流形导出后重新读回验证
尺寸差 1000 倍毫米/米单位混用检查导出单位和 scale
倒角失败半径超过面尺寸减小半径或跳过
孔位置偏移工作平面方向不对显式指定平面方向
仿真抖动惯性参数错误检查单位和公式
批量生成中断单个零件参数非法加 try-except 跳过

5.5 几条踩坑换来的经验

第一条,永远在导出后做一次回读验证。不要相信“导出没报错就是对的”。STEP 和 STL 都有可能在导出时静默丢失几何,回读一遍是最便宜的保险。

第二条,参数校验要前置。几何约束、单位一致性、必填项检查,全部放在构建之前。等模型生成出来再发现参数错了,排查成本高得多。

第三条,批量任务要能断点续跑。200 个零件跑到第 150 个崩了,如果从头再来会很崩溃。我的做法是每个零件生成后记录一个状态文件,重跑时跳过已完成的。

第四条,URDF 的碰撞体和视觉体可以不一样。视觉体用精细的 STL,碰撞体用简化的包围盒或圆柱,这样仿真速度快很多。很多人图省事两个用同一个,结果仿真慢得没法用。

第五条,文本描述要留扩展位。今天你只做法兰盘,明天可能要做支架。schema 设计时留一个type字段,解析层根据 type 分发到不同的构建函数。这样加新零件类型时不用改主流程。

6. 这条链路还能往哪延伸

跑通基础流程之后,我陆续加了一些扩展,每个都解决了一类实际问题。

参数化模板库。把常见的零件类型(法兰、支架、齿轮、连杆)都做成模板,每个模板对应一个 schema 和一个构建函数。文本解析层只需要识别“这是什么零件”,然后交给对应的模板。这样系统的覆盖面从单一零件扩展到一整个零件族。

与仿真流程打通。生成的 URDF 直接喂给仿真环境,跑一遍运动学验证,确认关节范围、碰撞体、惯性都正常。这一步能提前发现很多几何问题,比等到实物加工出来才发现强得多。

版本管理。每次生成的 STEP 和 URDF 都带一个哈希值,记录对应的参数和代码版本。这样出了问题能追溯到是哪一版参数、哪一版代码生成的。对于需要反复迭代的项目,这个习惯能省很多扯皮。

Web 接口。把整个流程包成一个 HTTP 服务,前端输入文本,后端返回 STEP 文件下载链接。这样非技术同事也能用,不用装 Python 环境。我用 FastAPI 包了一层,几十行代码就搞定了。

参数优化。给定目标(比如最小重量、最大刚度),用优化算法自动搜索参数组合,生成一批候选模型,再筛选。这一步把 text-to-cad 从“描述驱动”升级成“目标驱动”,是更有想象力的方向。

我个人在实际操作中的体会是,text-to-cad 最难的不是技术,而是“把几何意图说清楚”。人类描述一个零件时,脑子里有大量默认假设,比如“孔是贯穿的”“倒角是均匀的”“坐标系是居中的”。这些假设不写出来,程序就不知道。所以做这个项目的过程,其实也是逼自己把设计意图显式化的过程。一旦你把意图写清楚了,自动化就是水到渠成的事。

最后分享一个小技巧:如果你不确定某个参数该怎么描述,先在 CadQuery 里手工写一遍代码,跑通了再把代码里的数字抽出来变成参数。这样你得到的 schema 一定是可用的,不会出现“描述得挺好但代码实现不了”的情况。这个“先代码后参数”的顺序,比“先参数后代码”稳得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询