
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 python3.10 conda activate text2cad conda install -c conda-forge cadqueryPython 版本建议锁在 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())如果输出接近 100010×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_dim10000): 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, exportTypeSTEP, opt{write_pcurves: False, precision_mode: 0} )write_pcurves控制是否写入参数曲线关掉能减小文件体积precision_mode影响几何精度0 是默认数值越大精度越高但文件越大。对于要导入仿真软件的模型建议保持默认精度过高的精度反而会让仿真软件加载变慢。还有一个坑STEP 文件的单位。cadquery 默认按毫米处理导出的 STEP 也是毫米。但有些软件尤其是国外的默认按米读取导入后模型会小一千倍。如果遇到这种情况要么在导出时做单位转换要么在导入软件里手动设置单位。我一般是在导出前统一确认一遍避免下游出问题。5. URDF 生成与仿真对接让模型动起来5.1 URDF 和 STEP 的本质区别前面生成的 STEP 是死的几何它只描述形状不描述任何运动关系。而 URDFUnified 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 或 STLcq.exporters.export(link1, link1.step) cq.exporters.export(link2, link2.step)然后手写 URDF 把它们串起来robot namemy_robot link namebase_link visual geometry mesh filenamelink1.step/ /geometry /visual /link link namearm_link visual geometry mesh filenamelink2.step/ /geometry /visual /link joint namejoint1 typerevolute parent linkbase_link/ child linkarm_link/ origin xyz0 0 20 rpy0 0 0/ axis xyz0 0 1/ limit lower-1.57 upper1.57 effort10 velocity1.0/ /joint /robot5.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, encodingutf-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, encodingutf-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(processes4) 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: - python3.10 - cadquery2.4 - ocp7.7另外导出的 STEP 文件也要做版本管理。不同版本的 OpenCASCADE 导出的 STEP 在细节上可能有差异如果下游有严格的兼容性要求最好固定导出环境。8. 这条链路还能怎么延伸跑通基础流程之后我陆续加了一些扩展效果不错这里分享两个方向。一个是反向解析给一个已有的 STEP 文件程序自动提取它的特征参数尺寸、孔位、圆角半径等生成结构化的参数描述。这个在逆向工程和模型库管理里很有用——把一堆历史模型批量转成参数表后续就能用参数驱动的方式重新生成和修改。另一个是与优化算法结合把 text-to-cad 的生成能力当成一个函数输入参数输出模型然后接一个优化器去搜索最优参数。比如给定受力条件自动搜索最轻的支架结构。这个方向我还在摸索目前能做到简单的尺寸优化复杂拓扑优化还需要更专业的工具。最后说个实际体会text-to-cad 的价值不在于炫技而在于把重复劳动自动化。它不会取代设计师但能让设计师从画第 50 个相似的零件里解放出来去做真正需要创造力的工作。我现在的用法是标准件和规则件全部走自动生成自己只专注在那些真正需要判断和设计的部分。这个分工下来效率提升是实实在在的。