☰
text-to-cad实战:Python+OpenCASCADE实现文字转三维模型与URDF生成
2026/10/27 22:22:19 网站建设 项目流程

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

第一次听到 "text-to-cad" 这个词,很多人脑子里浮现的画面大概是:对着电脑敲一句"给我画一个长宽高 100×50×20 的带孔法兰盘",然后屏幕上就自动出现一个可以旋转查看的三维模型。这个画面听起来像是科幻,但它背后要打通的技术链路其实非常具体——自然语言理解、几何参数抽取、CAD 内核调用、文件格式导出,每一环都有明确的工程实现路径。

我最初接触这个方向,是因为手头有一批重复性极高的建模任务:几十个规格相近但尺寸不同的支架零件,每个都要在 CAD 里手动拉伸、打孔、倒角。做前三个还觉得新鲜,做到第十个就开始怀疑人生。当时就想,能不能让程序读一段描述,自动把模型生成出来?这就是 text-to-cad 最朴素的需求来源——把结构化的文字描述,翻译成结构化的几何数据,再交给 CAD 内核去执行。

需要先说清楚一个边界:text-to-cad 不是"AI 凭空画图",它的本质是参数化建模的自动化封装。真正干活的是 CAD 内核(比如 OpenCASCADE、FreeCAD 的 Part 模块),语言模型或者规则解析器只负责把"人话"转成"参数"。理解这一点非常关键,因为它决定了你的技术选型和预期——你不可能指望它生成一个造型极其复杂的曲面雕塑,但生成标准件、规则零件、参数化组件,它是完全胜任的。

这套东西适合谁?三类人最值得投入:一是机械设计工程师,手里有大量参数化零件需要批量生成;二是机器人方向的开发者,需要频繁产出 URDF 模型和对应的 STEP 几何文件;三是做自动化工具链的 Python 开发者,想把 CAD 能力集成进自己的脚本或服务里。如果你属于这三类中的任何一类,下面的内容基本可以照着抄。

关键词里出现的 CAD、STEP、URDF、Python,其实已经勾勒出了完整的技术地图:Python 是胶水语言,STEP 是几何交换格式,URDF 是机器人描述格式,CAD 是最终的应用场景。接下来我会把这条链路拆开,从环境搭建一直讲到实际跑通一个文字转模型的完整案例。

2. 技术选型:为什么是 Python + OpenCASCADE 这套组合

2.1 三种主流路线的取舍逻辑

在动手之前,我对比过三条技术路线,每条都有它的适用场景和坑。

第一条是直接调用商业 CAD 的 API,比如某些桌面 CAD 软件提供的二次开发接口。优点是内核成熟、精度高、导出格式全;缺点是依赖特定软件环境,部署成本高,而且很多接口对自动化调用并不友好,跑批量任务时容易卡在授权或界面交互上。我试过用这种方式做批量出图,单次调用没问题,一旦并发上来就各种异常,最后放弃了。

第二条是纯几何库自己造轮子,比如用 numpy 手撸网格顶点。这条路在简单形状上可行,但一旦涉及倒角、圆角、布尔运算,工作量会指数级上升,而且精度和鲁棒性很难保证。对于工程用途的 CAD 文件,这条路基本不现实。

第三条就是我现在主推的Python + OpenCASCADE(通过 cadquery 或 FreeCAD 的 Part 模块)。OpenCASCADE 是工业级的几何内核,支持 B-Rep(边界表示)建模,能输出 STEP、IGES、STL 等标准格式,而且有成熟的 Python 绑定。cadquery 这个库把 OpenCASCADE 的 API 封装得相当顺手,写起来接近"描述式建模"——你描述形状特征,它负责生成几何。

路线精度部署难度批量能力推荐场景
商业 CAD API高高弱单件精细设计
纯几何库低低中简单可视化
Python + OpenCASCADE高中强参数化批量生成

选第三条的核心理由是:它把"几何精度"和"自动化能力"这两个通常互斥的需求同时满足了。cadquery 的代码读起来几乎就是建模步骤的自然语言翻译,这对 text-to-cad 来说简直是天作之合——文字描述转成 cadquery 代码,比转成任何其他形式的中间表示都更直接。

2.2 环境搭建里最容易翻车的几个点

环境这块我踩过的坑足够写一篇避坑指南。先说结论:用 conda 而不是 pip 装 cadquery,能省掉 80% 的编译问题。

cadquery 依赖 OpenCASCADE 的底层库,pip 直接装经常会遇到编译失败,尤其是在 Windows 上,缺少 C++ 编译工具链或者版本不匹配,报错信息还特别晦涩。conda 的 conda-forge 频道有预编译好的二进制包,一条命令搞定:

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

Python 版本建议锁在 3.9 到 3.11 之间。我试过 3.12,当时 cadquery 的某些依赖还没跟上,装到一半就报错。3.10 是目前最稳的选择。

装完之后验证一下,跑这段代码:

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

如果输出接近 1000(10×10×10 的体积),说明环境没问题。如果报ImportError或者找不到 OCC 相关的库,八成是 conda 环境没激活对,或者混用了 pip 和 conda 装的包。切记不要在同一个环境里 pip 和 conda 混装 cadquery 及其依赖,这是最常见的翻车原因。

还有一个容易被忽略的点:STEP 导出需要额外的依赖。cadquery 导出 STEP 用的是 OCP 的接口,正常情况下 conda 装的时候会一起带上,但如果你是从其他途径装的,可能会缺。导出时如果报错说找不到 writer,先检查cq.exporters模块能不能正常导入。

2.3 为什么 STEP 和 URDF 要一起考虑

很多人做 text-to-cad 只想着导出 STEP 就完事了,但如果你的应用场景涉及机器人仿真,URDF 是绕不开的。STEP 描述的是几何形状,URDF 描述的是运动学结构——哪个是连杆、哪个是关节、关节怎么转、坐标系怎么定。两者是互补的,不是替代关系。

实际项目里我通常是这么处理的:先用 cadquery 生成每个零件的几何,导出 STEP 存档;然后根据零件的装配关系,手写或者程序生成 URDF,把 STEP 文件作为 visual 和 collision 的几何来源引用进去。这样一套流程下来,既有了可编辑的几何文件,又有了可直接导入仿真环境的描述文件。

关键词里出现 "urdf导入coppeliasim" 这个热搜词,说明很多人卡在 URDF 和仿真软件的对接上。这里先埋个伏笔,后面第 5 节会专门讲 URDF 生成和导入的细节。

3. 把一句话拆成参数:文本解析层的设计

3.1 规则解析 vs 大模型解析,怎么选

文本转参数这一步,是整个 text-to-cad 里最"软"的部分,也是最能体现设计功力的地方。我试过两种方案,各有各的适用场景。

规则解析就是用正则表达式或者关键词匹配,从固定格式的描述里抽参数。比如"长 100 宽 50 高 20 的方块",直接匹配数字和单位词。优点是确定性高、速度快、不依赖外部服务,缺点是只能处理你预先定义好的句式,稍微换个说法就歇菜。

大模型解析是把描述丢给语言模型,让它输出结构化的 JSON 参数。优点是容错性强、能理解自然表达,比如"一个差不多巴掌大的方形底座"它也能猜出个大概尺寸;缺点是有不确定性、需要联网或本地部署、成本更高。

我的实际做法是混合:先用规则解析尝试抽取,抽到了就用;抽不到或者置信度低,再走大模型兜底。这样既保证了常见情况的效率和稳定,又保留了处理复杂表达的能力。

import re def parse_box(text): # 尝试匹配 "长X宽Y高Z" 或 "X乘Y乘Z" 等模式 pattern = r'(\d+(?:\.\d+)?)\s*[乘xX×*]\s*(\d+(?:\.\d+)?)\s*[乘xX×*]\s*(\d+(?:\.\d+)?)' match = re.search(pattern, text) if match: return { "length": float(match.group(1)), "width": float(match.group(2)), "height": float(match.group(3)) } return None

这段代码能处理"100×50×20"、"100乘50乘20"、"100 x 50 x 20"这几种写法。实测下来,覆盖了大部分工程场景里的尺寸描述。

3.2 参数校验:别让错误尺寸流到几何层

解析出参数之后,必须做校验,这是很多人会跳过但极其重要的一步。我见过因为没校验,程序拿着一个负数尺寸去建模,OpenCASCADE 直接抛异常,整个批处理任务中断的情况。

校验至少包括三项:数值范围(尺寸必须为正、不能超过某个上限)、单位统一(毫米还是米,必须明确)、逻辑一致性(比如孔径不能大于零件宽度)。

def validate_params(params, max_dim=10000): for key, value in params.items(): if not isinstance(value, (int, float)): raise ValueError(f"{key} 不是数值: {value}") if value <= 0: raise ValueError(f"{key} 必须为正数: {value}") if value > max_dim: raise ValueError(f"{key} 超出上限 {max_dim}: {value}") return True

单位这块特别容易出问题。CAD 领域默认单位通常是毫米,但有些场景用米,有些用英寸。我的建议是在解析层就统一转成毫米,后续所有几何操作都用毫米,导出时再按需转换。这样能避免"模型看起来正常但尺寸差了一千倍"这种低级错误。

3.3 从参数到建模代码的映射策略

参数有了,接下来要决定怎么把它变成几何。这里有两种思路:模板化和代码生成。

模板化是预先写好几种常见形状的建模函数(方块、圆柱、带孔板、L 型支架等),解析出参数后直接调用对应函数。优点是可控、可测试、性能好;缺点是形状种类有限,遇到没预定义的形状就没辙。

代码生成是根据描述动态拼出 cadquery 代码字符串,然后 exec 执行。优点是灵活度极高,理论上能生成任意形状;缺点是安全风险大(exec 执行任意代码)、调试困难、生成的代码质量参差不齐。

我目前用的是模板化为主、代码生成为辅的策略。80% 的需求用模板覆盖,剩下 20% 的特殊形状再考虑动态生成,而且动态生成的部分会做严格的沙箱限制。对于绝大多数工程场景,模板化已经够用了——毕竟标准件和规则零件才是批量生成的主力。

4. 几何生成实战:用 cadquery 把参数变成实体

4.1 一个完整的带孔法兰盘案例

光说不练假把式,这里用一个完整的例子把流程串起来。需求描述是:"生成一个外径 80、内径 30、厚度 10 的法兰盘,沿圆周均匀分布 6 个直径 8 的螺栓孔,孔中心距外缘 10。"

先解析参数:

params = { "outer_dia": 80.0, "inner_dia": 30.0, "thickness": 10.0, "hole_count": 6, "hole_dia": 8.0, "hole_offset": 10.0 }

然后建模。cadquery 的链式 API 写起来非常直观:

import cadquery as cq import math def make_flange(p): # 基础圆盘 result = ( cq.Workplane("XY") .circle(p["outer_dia"] / 2) .extrude(p["thickness"]) ) # 中心通孔 result = result.faces(">Z").workplane().hole(p["inner_dia"]) # 螺栓孔分布圆半径 bolt_circle_r = p["outer_dia"] / 2 - p["hole_offset"] # 在分布圆上打孔 result = ( result.faces(">Z").workplane() .polarArray(bolt_circle_r, 0, 360, p["hole_count"]) .hole(p["hole_dia"]) ) return result flange = make_flange(params) cq.exporters.export(flange, "flange.step")

这段代码跑完,当前目录下就会生成一个flange.step文件,用任何支持 STEP 的软件都能打开。整个过程从参数到文件,不到 20 行代码。

4.2 布尔运算的顺序会影响成败

上面这个例子看起来简单,但里面藏着一个关键经验:布尔运算的顺序很重要。我一开始的写法是先打中心孔,再打螺栓孔,结果发现螺栓孔有时候会跟中心孔"打架"——如果分布圆半径设小了,螺栓孔会和中心孔重叠,OpenCASCADE 在布尔减运算时可能产生退化面或者直接失败。

正确的做法是先做大的减运算,再做小的,或者干脆把所有要减去的形状先合并成一个,再一次性减。后者更稳妥:

# 先把所有孔合并成一个"负形状" holes = cq.Workplane("XY").circle(p["inner_dia"] / 2).extrude(p["thickness"]) bolt_circle_r = p["outer_dia"] / 2 - p["hole_offset"] holes = ( holes.faces(">Z").workplane() .polarArray(bolt_circle_r, 0, 360, p["hole_count"]) .circle(p["hole_dia"] / 2) .extrude(p["thickness"]) ) # 一次性减掉 result = base.cut(holes)

这样做的另一个好处是性能更好。多次布尔运算比一次慢得多,尤其是孔数量多的时候,差距很明显。我做过测试,20 个孔的零件,一次性减比逐个减快将近 3 倍。

4.3 导出 STEP 时的精度与格式陷阱

导出这一步看似简单,其实也有讲究。cq.exporters.export默认的 STEP 精度对大多数场景够用,但如果你做的是精密零件,可能需要调整。

from cadquery import exporters exporters.export( flange, "flange.step", exportType="STEP", opt={"write_pcurves": False, "precision_mode": 0} )

write_pcurves控制是否写入参数曲线,关掉能减小文件体积;precision_mode影响几何精度,0 是默认,数值越大精度越高但文件越大。对于要导入仿真软件的模型,建议保持默认精度,过高的精度反而会让仿真软件加载变慢。

还有一个坑:STEP 文件的单位。cadquery 默认按毫米处理,导出的 STEP 也是毫米。但有些软件(尤其是国外的)默认按米读取,导入后模型会小一千倍。如果遇到这种情况,要么在导出时做单位转换,要么在导入软件里手动设置单位。我一般是在导出前统一确认一遍,避免下游出问题。

5. URDF 生成与仿真对接:让模型动起来

5.1 URDF 和 STEP 的本质区别

前面生成的 STEP 是"死"的几何,它只描述形状,不描述任何运动关系。而 URDF(Unified Robot Description Format)是"活"的,它描述的是连杆(link)和关节(joint)组成的运动学树。一个 URDF 文件里,每个 link 有自己的几何(可以引用 STEP 或 STL),每个 joint 定义了父子 link 之间的连接方式和运动约束。

举个直观的例子:一个机械臂,底座、大臂、小臂、末端执行器是四个 link,它们之间的旋转关节是三个 joint。URDF 要描述的就是"谁连着谁、怎么连、能怎么动"。几何只是其中一部分,更重要的是坐标系变换和运动约束。

很多人做 text-to-cad 只做到 STEP 就停了,但如果目标是机器人仿真,URDF 才是终点。关键词里 "urdf导入coppeliasim" 这个热搜词,说明这个环节的痛点很集中。

5.2 从几何到 URDF 的坐标系对齐

URDF 里最容易出错的地方是坐标系。每个 link 都有自己的局部坐标系,joint 定义的是父 link 坐标系到子 link 坐标系的变换。如果坐标系没对齐,导入仿真软件后模型会错位、关节会绕错误的轴旋转。

我的经验是:在 cadquery 里建模时就把坐标系定好,让每个零件的原点位于它和父零件连接的位置。这样导出后,URDF 里的 joint origin 直接写零向量就行,省去大量调试。

# 建模时把原点放在连接点 link1 = cq.Workplane("XY").box(100, 50, 20) # 原点在底面中心 link2 = ( cq.Workplane("XY") .center(50, 0) # 偏移到连接位置 .box(80, 40, 15) )

导出时,每个 link 单独导出一个 STEP 或 STL:

cq.exporters.export(link1, "link1.step") cq.exporters.export(link2, "link2.step")

然后手写 URDF 把它们串起来:

<robot name="my_robot"> <link name="base_link"> <visual> <geometry> <mesh filename="link1.step"/> </geometry> </visual> </link> <link name="arm_link"> <visual> <geometry> <mesh filename="link2.step"/> </geometry> </visual> </link> <joint name="joint1" type="revolute"> <parent link="base_link"/> <child link="arm_link"/> <origin xyz="0 0 20" rpy="0 0 0"/> <axis xyz="0 0 1"/> <limit lower="-1.57" upper="1.57" effort="10" velocity="1.0"/> </joint> </robot>

5.3 导入仿真软件时的常见报错与修复

URDF 写好了,导入仿真软件时还是可能翻车。我整理了几个高频问题:

报错现象根本原因修复方法
模型不显示mesh 路径错误用绝对路径或确认相对路径基准
模型全黑缺少材质定义在 visual 里加 material 标签
关节转不动axis 定义错误检查旋转轴方向是否与几何匹配
模型比例异常单位不一致确认 mesh 单位和 URDF 单位一致
碰撞检测失效缺少 collision 标签补上 collision 几何定义

其中路径问题是最常见的。URDF 里的 mesh 路径可以是相对路径也可以是绝对路径,相对路径的基准是 URDF 文件所在目录。我建议用package://形式(ROS 生态)或者干脆用绝对路径,避免因为工作目录变化导致找不到文件。

还有一个细节:STEP 文件在仿真软件里的支持度不如 STL。有些仿真软件对 STEP 的解析不完整,导入后模型会有破面。如果遇到这种情况,把 STEP 转成 STL 再引用,虽然损失了参数化信息,但显示和碰撞检测会更稳定。

6. 批量生成与工程化:从玩具到工具

6.1 批量任务的参数表驱动模式

单个模型生成跑通了,接下来就是批量。批量生成的核心思路是参数表驱动——把每个零件的参数写成一行,程序读一行生成一个模型。

参数表可以用 CSV、Excel 或者 JSON。我偏好 CSV,因为工程人员用 Excel 编辑方便,导出 CSV 也简单。

import csv def batch_generate(csv_path, output_dir): with open(csv_path, newline='', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: params = {k: float(v) for k, v in row.items() if k != 'name'} model = make_flange(params) out_path = f"{output_dir}/{row['name']}.step" cq.exporters.export(model, out_path) print(f"生成: {out_path}")

这个模式的好处是非程序员也能参与——工程师在 Excel 里填参数,跑一下脚本就出模型,不需要懂 Python。我在实际项目里用这种方式,把原本需要几天的建模工作压缩到了几分钟。

6.2 异常处理:别让一个坏参数毁掉整批任务

批量任务最怕的就是中途崩溃。第 37 个零件的参数有问题,程序抛异常退出,前面 36 个白跑了。所以异常处理必须做。

def batch_generate_safe(csv_path, output_dir): success, failed = [], [] with open(csv_path, newline='', encoding='utf-8') as f: reader = csv.DictReader(f) for row in reader: try: params = {k: float(v) for k, v in row.items() if k != 'name'} validate_params(params) model = make_flange(params) out_path = f"{output_dir}/{row['name']}.step" cq.exporters.export(model, out_path) success.append(row['name']) except Exception as e: failed.append((row['name'], str(e))) print(f"失败: {row['name']} - {e}") print(f"成功 {len(success)} 个,失败 {len(failed)} 个") return success, failed

关键是每个零件独立 try-except,一个失败不影响其他。最后汇总报告,把失败的零件和原因列出来,方便人工排查。

6.3 性能优化:并行与缓存

零件数量上百之后,串行生成会明显变慢。这时候可以考虑并行。cadquery 的几何运算本身是 CPU 密集型的,用多进程并行效果不错:

from multiprocessing import Pool def generate_one(args): params, out_path = args model = make_flange(params) cq.exporters.export(model, out_path) return out_path with Pool(processes=4) as pool: results = pool.map(generate_one, task_list)

进程数建议设成 CPU 核心数,不要开太多,否则上下文切换反而拖慢速度。我实测下来,4 核机器开 4 个进程,速度大约是串行的 3.5 倍。

另一个优化点是缓存。如果同一批任务里有重复的参数组合,可以缓存已经生成过的模型,直接复制文件而不是重新计算。这个在参数表有大量重复行的时候特别有效。

7. 踩过的坑与实战心得

7.1 几何内核的"脾气":什么时候会失败

OpenCASCADE 虽然强大,但它不是万能的。有些几何操作会失败,而且失败原因往往很隐蔽。我遇到过的典型情况:

自相交。当你用布尔运算减去的形状和原形状有复杂的相交关系时,内核可能算不出结果。比如一个孔刚好和零件边缘相切,或者两个孔部分重叠。解决办法是在设计参数时就避免这种临界情况,比如孔中心距边缘至少留一个孔半径的距离。

退化面。当某个尺寸趋近于零时(比如倒角半径大于零件厚度的一半),会生成退化面,后续操作可能出错。解决办法是加参数校验,把不合理的尺寸挡在建模之前。

精度问题。浮点数运算的累积误差可能导致两个本该重合的面有微小间隙,布尔运算时被判定为不相交。解决办法是在关键操作前做容差处理,或者调整内核的容差参数。

这些坑的共同点是:它们不会在简单案例里出现,只会在参数接近边界时暴露。所以测试的时候一定要用极端参数跑一遍,别只用"好看"的参数测。

7.2 文本描述的歧义怎么处理

自然语言最大的问题是歧义。"一个 100 的圆"——是直径 100 还是半径 100?"厚度 5"——是毫米还是厘米?"打几个孔"——几个是几个?

我的处理原则是:能问就问,不能问就按最保守的默认值。在交互式场景里,解析出歧义参数时直接反问用户确认;在批量场景里,用行业默认值(直径、毫米、最少孔数),并在日志里标注"使用了默认值",方便事后核查。

还有一个技巧是在参数表里强制明确单位。CSV 表头写成outer_dia_mm而不是outer_dia,从源头消除单位歧义。这个习惯能省掉大量沟通成本。

7.3 版本兼容性:库更新带来的意外

cadquery 和 OpenCASCADE 都在持续更新,版本升级有时会带来 API 变化。我遇到过升级后polarArray的参数顺序变了,导致生成的孔位置全错的情况。

应对策略是锁定版本。在项目里用requirements.txt或environment.yml明确写死版本号,不要用latest。升级前先在测试环境跑一遍回归测试,确认所有模型生成结果一致再上生产。

# environment.yml dependencies: - python=3.10 - cadquery=2.4 - ocp=7.7

另外,导出的 STEP 文件也要做版本管理。不同版本的 OpenCASCADE 导出的 STEP 在细节上可能有差异,如果下游有严格的兼容性要求,最好固定导出环境。

8. 这条链路还能怎么延伸

跑通基础流程之后,我陆续加了一些扩展,效果不错,这里分享两个方向。

一个是反向解析:给一个已有的 STEP 文件,程序自动提取它的特征参数(尺寸、孔位、圆角半径等),生成结构化的参数描述。这个在逆向工程和模型库管理里很有用——把一堆历史模型批量转成参数表,后续就能用参数驱动的方式重新生成和修改。

另一个是与优化算法结合:把 text-to-cad 的生成能力当成一个"函数",输入参数输出模型,然后接一个优化器去搜索最优参数。比如给定受力条件,自动搜索最轻的支架结构。这个方向我还在摸索,目前能做到简单的尺寸优化,复杂拓扑优化还需要更专业的工具。

最后说个实际体会:text-to-cad 的价值不在于"炫技",而在于把重复劳动自动化。它不会取代设计师,但能让设计师从"画第 50 个相似的零件"里解放出来,去做真正需要创造力的工作。我现在的用法是,标准件和规则件全部走自动生成,自己只专注在那些真正需要判断和设计的部分。这个分工下来,效率提升是实实在在的。

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

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

立即咨询