
1. 从一段文字到三维模型text-to-cad 到底在解决什么问题第一次听到 text-to-cad 这个词很多人会下意识觉得它离自己很远像是实验室里的概念。但如果你真的动手做过参数化建模就会明白它戳中的是一个特别具体的痛点我们脑子里有一个零件的形状却要花大量时间在软件里点草图、拉尺寸、加约束最后才得到一个能导出的模型。text-to-cad 想做的事情就是把“描述”直接变成“几何”中间那些重复的鼠标操作尽量交给程序去完成。我自己接触这个方向是从批量生成机械臂连杆开始的。当时手头有几十个规格相近的连杆长度、孔径、倒角各不相同如果一个个在图形界面里画一天都做不完。后来我改用 Python 脚本驱动几何内核把每个连杆的参数写进一个字典循环生成 STEP 文件再顺手导出 URDF 给仿真环境用。那套流程跑通之后我才真正理解 text-to-cad 的价值它不是要取代 CAD 工程师而是把“重复劳动”和“参数化变体”这两块吃下来让人专注在真正需要判断力的设计决策上。这篇文章面向的读者很明确会一点 Python、对 CAD 有基本概念、想把建模流程自动化的人。你不需要是几何内核专家但最好知道什么是 STEP、什么是 URDF、什么是边界表示BRep。我会从整体思路讲到具体实现把参数计算、代码结构、踩过的坑都摊开说。核心关键词 text-to-cad、CAD、STEP、URDF、Python 会贯穿全文因为它们本来就是一条链上的东西文本描述进Python 处理CAD 几何出STEP 存档URDF 喂给仿真。先说清楚一个容易混淆的点。text-to-cad 里的“text”不一定是指自然语言大模型那种自由文本它更常见的形态是结构化的参数描述比如 JSON、YAML或者一段带变量的 Python 字典。真正用大语言模型把一句话变成几何目前还停留在比较初级的阶段生成的多是简单形体尺寸精度和工程可用性都有限。所以务实的做法是用结构化文本做“可控输入”把自然语言当作辅助入口。这个判断很重要它决定了你后面整套系统的稳定性。2. 整体设计与技术选型为什么是 Python 加几何内核2.1 核心思路拆解文本层、几何层、导出层三段式我把整个 text-to-cad 流程拆成三层这个分层是我踩了不少坑之后固定下来的。最上面是文本层负责接收参数、做校验、填默认值中间是几何层调用几何内核把参数变成实体最下面是导出层把实体写成 STEP、STL 或者 URDF。三层之间用纯数据结构通信不互相依赖具体实现这样任何一层换技术栈都不会牵动全局。为什么强调这个分层因为我早期写过一个“一把梭”的脚本参数解析、建模、导出全揉在一个函数里结果想换个导出格式就得改建模代码想加个参数校验又怕碰坏几何逻辑。后来拆开之后几何层只认一个标准的参数字典导出层只认一个实体对象维护成本立刻降下来。这个经验对任何做自动化建模的人都适用先把边界划清楚再谈具体实现。文本层的关键是“可校验”。参数进来之后先检查必填项、类型、取值范围不合法就直接报错绝不带着脏数据往下走。我见过太多脚本因为一个负数的孔径导致内核直接崩溃排查半天才发现是输入问题。所以文本层要有一份 schema哪怕只是简单的字典校验也能挡掉大部分低级错误。2.2 几何内核怎么选FreeCAD、CadQuery、build123d 的取舍几何内核是整个系统的发动机选错了后面全是麻烦。目前 Python 生态里主流的几条路线我一个个说。FreeCAD 是最老牌的选择它本身是个完整的 CAD 软件Python 接口能直接操作它的文档对象。优点是功能全草图、约束、布尔运算、装配都有缺点是 API 偏底层写起来啰嗦而且它依赖图形环境在纯服务器上跑要额外配置。我早期用它做过一批法兰盘生成能跑通但代码量大调试也慢。CadQuery 是我用得最顺手的。它基于 OpenCASCADEOCCT这个工业级几何内核API 设计得很“Pythonic”一行链式调用就能完成拉伸、倒角、开孔。比如cq.Workplane(XY).circle(10).extrude(5)就是一个圆柱。它的定位就是“脚本化 CAD”非常适合 text-to-cad 这种场景。缺点是文档更新跟不上版本有些高级功能要翻源码。build123d 是 CadQuery 团队后来推的新库思路更接近“用 Python 写 CAD 的领域语言”支持上下文管理器和运算符重载写复杂装配比 CadQuery 更清晰。它同样基于 OCCT导出 STEP 没问题。如果你是新项目我建议直接从 build123d 起步如果团队已经在用 CadQuery也没必要急着迁移。选型的时候有个硬指标必须能导出 STEP。STEP 是工程交换的事实标准URDF 里的视觉和碰撞网格虽然常用 STL但真正要拿去加工或者做精确装配还是得靠 STEP。上面三个库都能导出 STEP这一点上打平。真正拉开差距的是 API 手感和调试体验这个只能自己上手试。2.3 为什么导出层要同时支持 STEP 和 URDF很多人会问既然有了 STEP为什么还要 URDF因为这两个东西服务的对象完全不同。STEP 是给制造和 CAD 软件看的它描述的是精确的边界表示一个孔就是真正的圆柱面。URDF 是给机器人仿真看的它描述的是连杆、关节、坐标系之间的运动学关系几何只是附属的视觉和碰撞体。我做过一个六轴机械臂的项目几何模型在 CAD 里画好导出 STEP但要在仿真环境里动起来必须转成 URDF。URDF 里每个 link 的视觉网格可以用 STL碰撞体可以用简化后的凸包关节的旋转轴和限位要单独定义。这些信息 STEP 里根本没有所以导出层必须能同时产出两种格式并且保证它们来自同一套参数否则改了一个尺寸忘了同步另一个仿真和实物就对不上。这里有个实操细节URDF 的坐标系约定和 CAD 的坐标系约定经常不一致。CAD 里习惯 Z 轴朝上URDF 里很多机器人模型习惯 Z 轴朝前或者朝上但关节轴方向不同。我的做法是在导出层加一个坐标变换矩阵把 CAD 的几何统一变换到 URDF 期望的坐标系而不是去改几何本身。这样几何层保持干净坐标问题在导出层一次性解决。3. 核心细节解析参数、几何与导出的关键要点3.1 参数描述怎么设计才不容易翻车参数描述是 text-to-cad 的入口设计得好后面顺风顺水设计得烂天天救火。我的原则是扁平优先、单位统一、默认值明确、命名见名知意。扁平优先的意思是能用一层字典解决就不要嵌套三层。比如一个带孔的板参数写成{length: 100, width: 60, thickness: 5, hole_dia: 8, hole_count: 4}就够了不要搞成{geometry: {base: {size: {...}}}}这种。嵌套深了取值路径长校验也麻烦。当然如果是复杂装配该分层还是要分层但单个零件尽量扁平。单位统一是血泪教训。我见过一个项目长度参数有的用毫米有的用米结果生成的模型大了 1000 倍导进仿真直接飞出去。后来我强制规定所有长度单位一律毫米角度一律弧度在文本层入口就做转换和标注。OCCT 内部对单位不敏感它只认数值所以单位一致性必须由上层保证。默认值要明确。不是每个参数都必须由用户提供像倒角半径、圆角大小这种给个合理的默认值能省很多事。但默认值必须写进 schema不能藏在代码逻辑里否则用户不知道有这个参数存在。我习惯把默认值和取值范围写在一起形成一份“参数说明书”既是文档也是校验依据。命名见名知意这点看似废话但真的重要。d和dia和hole_diameter哪个一眼能懂在团队协作里参数名就是接口改一次名字所有调用方都得跟着改所以一开始就要起好。3.2 几何建模的常见操作与避坑点几何层最常用的操作就那么几个拉伸、旋转、扫掠、布尔运算、倒角、抽壳。每个都有坑。拉伸extrude是最基础的但要注意草图平面的方向和拉伸方向。CadQuery 里Workplane(XY)默认在 XY 平面拉伸沿 Z 轴正方向。如果你想要反向拉伸得用extrude(-thickness)或者先workplane再翻转。我踩过的坑是草图在 XY 平面但拉伸方向搞反了模型跑到负 Z 区域导出后和别的零件对不上。布尔运算boolean是重灾区。两个实体做差集如果它们只是相切或者有极小的重叠OCCT 有时会算出空集或者报错。解决办法是让重叠区域足够大或者用“模糊布尔”fuzzy boolean容差。CadQuery 的cut和union底层就是 OCCT 的布尔遇到诡异结果先检查两个实体是不是真的相交。倒角fillet和圆角chamfer要小心边选择。自动选择所有边做倒角在简单形体上没问题但复杂形体上可能选到不该倒的边导致几何失败。我的做法是显式指定要倒角的边用坐标或者边索引来选虽然麻烦但可控。如果非要自动选先在小模型上验证再放到大模型上跑。抽壳shell对壁厚有要求。壁厚太薄OCCT 可能算不出来壁厚太厚可能超过实体本身的尺寸。一般壁厚不要小于最小特征尺寸的十分之一。这个没有硬性公式靠经验多试几次就有感觉了。3.3 STEP 导出的参数与注意事项STEP 导出看起来简单其实有几个参数会影响结果。最关键是协议版本和单位。OCCT 默认导出 AP214 或者 AP203AP214 支持颜色和装配AP203 更老但兼容性好。如果只是单个零件AP203 够用如果要保留颜色和装配结构用 AP214。单位问题前面提过STEP 文件里会写明单位但有些老软件读的时候会忽略导致尺寸错乱。我的习惯是在导出前确认几何的数值范围比如一个 100mm 的零件导出后文件里应该是 100 左右如果变成 0.1 或者 100000那就是单位出问题了。还有一个坑是精度。STEP 是精确表示理论上没有精度损失但 OCCT 在计算过程中会有浮点误差。对于一般机械零件默认精度足够如果做光学或者精密配合可能需要调高内核的容差。这个一般用不到知道有这回事就行。导出路径也要注意。中文路径在某些环境下会出问题我一般用纯英文路径文件名也用英文加数字避免编码麻烦。3.4 URDF 导出的结构映射与坐标系处理URDF 导出比 STEP 复杂因为它涉及运动学结构。一个 URDF 文件里link 是刚体joint 是关节每个 link 有自己的视觉和碰撞几何每个 joint 有类型、父 link、子 link、原点变换和轴向。从 text-to-cad 的角度参数描述里要额外包含运动学信息。比如一个连杆除了几何尺寸还要知道它的旋转轴在哪、关节限位是多少、和哪个 link 相连。这些信息在纯几何参数里是没有的所以文本层要扩展把运动学参数也纳入进来。坐标系处理是 URDF 导出的核心难点。CAD 里一个零件通常以自身某个特征为原点但 URDF 里 link 的原点要和 joint 的原点对齐。我的做法是在几何层生成零件时就按 URDF 期望的坐标系来建让零件的原点直接落在关节旋转轴上。这样导出时不需要额外变换减少出错概率。视觉和碰撞网格的处理也有讲究。视觉网格可以用精细的 STL碰撞网格最好用简化后的凸包或者包围盒否则仿真时碰撞检测会非常慢。我一般用 trimesh 库做凸包简化把几万个三角面降到几百个仿真速度立刻上来。4. 实操过程从零搭一套可复现的 text-to-cad 流程4.1 环境准备与依赖安装先把环境搭起来。我推荐用 Python 3.10 或 3.11太老的版本有些库不支持太新的版本有些库还没跟上。虚拟环境用 venv 或者 conda 都行我习惯 venv轻量。python -m venv tcad-env source tcad-env/bin/activate # Windows 用 tcad-env\Scripts\activate pip install cadquery build123d trimesh numpyCadQuery 和 build123d 都依赖 OCCTpip 安装时会自动拉预编译的二进制包一般不会出问题。如果网络慢可以换国内镜像源。trimesh 用来处理 STL 和凸包numpy 做数值计算。验证安装import cadquery as cq result cq.Workplane(XY).circle(10).extrude(5) cq.exporters.export(result, test.step) print(OK)能生成 test.step 就说明环境没问题。如果报错说找不到 OCCT检查一下是不是装了两个版本的 cadquery 冲突了卸载重装通常能解决。4.2 参数描述文件的设计与校验我用 YAML 写参数因为可读性好注释也方便。一个带孔板的参数文件长这样name: mounting_plate length: 100.0 width: 60.0 thickness: 5.0 hole_dia: 8.0 hole_count: 4 hole_margin: 10.0 fillet_radius: 2.0校验用 pydantic 或者简单的字典检查。我倾向 pydantic因为它能自动做类型转换和范围校验还能生成 JSON Schema 给前端用。from pydantic import BaseModel, Field class PlateParams(BaseModel): name: str length: float Field(gt0) width: float Field(gt0) thickness: float Field(gt0) hole_dia: float Field(gt0) hole_count: int Field(ge0) hole_margin: float Field(ge0) fillet_radius: float Field(ge0)这样参数进来先过一遍模型不合法直接抛异常几何层永远拿到的是干净数据。这个习惯能省掉大量调试时间。4.3 几何生成代码的完整实现下面是一个完整的带孔板生成函数用 CadQuery 写import cadquery as cq from pydantic import BaseModel, Field class PlateParams(BaseModel): name: str length: float Field(gt0) width: float Field(gt0) thickness: float Field(gt0) hole_dia: float Field(gt0) hole_count: int Field(ge0) hole_margin: float Field(ge0) fillet_radius: float Field(ge0) def build_plate(p: PlateParams): # 基础板 plate cq.Workplane(XY).box(p.length, p.width, p.thickness) # 四角倒圆 if p.fillet_radius 0: plate plate.edges(|Z).fillet(p.fillet_radius) # 打孔 if p.hole_count 0: # 计算孔位四角各一个按 margin 内缩 x p.length / 2 - p.hole_margin y p.width / 2 - p.hole_margin points [(-x, -y), (x, -y), (x, y), (-x, y)] # 如果孔数超过4简单处理为只取前4个 points points[:p.hole_count] plate ( plate.faces(Z) .workplane() .pushPoints(points) .hole(p.hole_dia) ) return plate这段代码有几个细节值得说。edges(|Z)选的是平行于 Z 轴的边也就是四个竖直棱边对它们倒圆就是四角圆角。faces(Z)选的是 Z 方向最高的面在它上面建工作平面打孔孔的方向自然朝下。pushPoints接受一个坐标列表hole直接打穿。参数计算上孔位用length/2 - margin得到 X 方向偏移width/2 - margin得到 Y 方向偏移四个角对称分布。这个逻辑假设孔是均匀分布在四角如果实际需求是别的分布改points列表就行。4.4 导出 STEP 与 URDF 的完整流程生成实体之后导出 STEP 很简单cq.exporters.export(plate, f{p.name}.step)导出 URDF 要复杂一些因为要构造 XML 结构。我用一个简化的例子说明假设这个板是一个 link没有关节import trimesh from urdfpy import URDF # 或者手写 XML # 先导出 STL 作为视觉网格 cq.exporters.export(plate, f{p.name}.stl) # 用 trimesh 做凸包简化作为碰撞网格 mesh trimesh.load(f{p.name}.stl) hull mesh.convex_hull hull.export(f{p.name}_collision.stl) # 手写 URDF XML urdf_template f?xml version1.0? robot name{p.name} link name{p.name}_link visual geometry mesh filename{p.name}.stl/ /geometry /visual collision geometry mesh filename{p.name}_collision.stl/ /geometry /collision /link /robot with open(f{p.name}.urdf, w) as f: f.write(urdf_template)这个例子很简化真实项目里 link 和 joint 会多得多但结构是一样的。关键是视觉网格用精细 STL碰撞网格用凸包两者文件名要对应上。4.5 批量生成与参数扫描text-to-cad 真正的威力在批量。假设你要生成 20 个不同长度的板只要循环改参数import yaml base yaml.safe_load(open(base_plate.yaml)) for i, length in enumerate(range(80, 120, 2)): base[length] length base[name] fplate_{i:02d} p PlateParams(**base) solid build_plate(p) cq.exporters.export(solid, foutput/{p.name}.step)这段代码会生成 20 个 STEP 文件长度从 80 到 118 毫米。如果还要导出 URDF把导出逻辑也放进循环。批量生成的时候要注意文件命名唯一否则会互相覆盖。我习惯用序号加参数值命名比如plate_080一眼能看出尺寸。参数扫描还有个进阶用法用 numpy 做网格搜索生成参数组合再并行跑。CadQuery 本身不是线程安全的但可以用多进程每个进程独立生成一批。这个在参数空间大的时候很有用。5. 常见问题与排查技巧实录5.1 几何内核报错怎么定位OCCT 的报错信息经常很模糊比如 BRep_API: command not done看不出哪里错了。我的排查顺序是先简化模型把倒角、抽壳这些容易出问题的操作去掉看基础形体能不能生成如果能再一个个加回来定位到具体操作。然后检查那个操作的输入比如倒角半径是不是大于了相邻面的尺寸布尔运算的两个实体是不是真的相交。还有一个常见原因是自相交。草图如果有自相交的线段拉伸出来的实体就是坏的后续操作全崩。检查方法是把草图单独导出来看或者用isValid()检查实体有效性。5.2 STEP 导出后打不开或显示异常STEP 打不开通常是文件没写完或者格式不对。先检查文件大小如果是 0 字节说明导出过程报错了但没抛异常。CadQuery 的 export 有时会静默失败最好在导出后检查文件是否存在且大小合理。显示异常比如模型缺面、破面多半是几何本身有问题。用solid.isValid()检查如果返回 False回到几何层修。还有一种情况是单位问题前面说过模型尺寸差了几个数量级看起来就像消失了。5.3 URDF 导入仿真环境后位置错乱URDF 位置错乱几乎都是坐标系问题。先检查每个 joint 的 origin 变换xyz 和 rpy 是不是符合预期。然后检查 link 的视觉网格原点STL 的原点是不是在 link 的原点上。如果 STL 是从 CAD 导出的CAD 的原点可能不在你期望的位置需要在导出前把零件平移到正确位置。还有一个坑是关节轴向。URDF 里 joint 的 axis 是单位向量如果写错方向关节会绕错误的轴旋转。检查方法是把仿真里的关节手动转一下看旋转方向对不对。5.4 常见问题速查表问题现象可能原因排查方法解决方式内核报错 command not done几何操作输入非法逐步简化模型定位修正参数或调整操作顺序STEP 文件 0 字节导出时静默失败检查文件大小捕获异常检查实体有效性模型尺寸差数量级单位不一致检查参数单位统一为毫米URDF 位置错乱坐标系不匹配检查 joint origin在导出层做坐标变换仿真碰撞检测慢碰撞网格太精细查看三角面数量用凸包简化倒角失败半径过大或边选择错误减小半径或显式选边调整半径手动指定边布尔运算结果为空实体不相交或相切检查实体位置增大重叠区域5.5 几个我踩过的坑和独家技巧第一个坑是浮点精度。两个本该重合的面因为浮点误差差了 1e-10布尔运算就出问题。解决办法是在关键尺寸上做圆整比如把 10.000000001 圆整到 10.0。CadQuery 里可以用round或者在内核层面设容差。第二个坑是文件句柄泄漏。批量生成几千个文件时如果不用with open文件句柄会耗尽程序崩溃。所有文件操作都用上下文管理器这是铁律。第三个技巧是缓存中间结果。几何生成很慢如果参数没变没必要重新生成。我用文件哈希做缓存键参数文件的哈希值作为文件名命中就直接读省时间。第四个技巧是日志。批量生成时一定要打日志记录每个参数的生成结果和耗时。出问题时能快速定位是哪个参数组合导致的不用从头跑。6. 后续可以怎么扩展这套流程跑通基础流程之后有几个方向可以继续深挖。一个是接入自然语言入口用大语言模型把一句话解析成参数字典再走现有流程。这个方向目前精度有限但作为辅助输入很有价值比如用户说“生成一个 100 毫米长的板”模型输出{length: 100}剩下的交给确定性代码。另一个方向是加装配约束。现在生成的是单个零件如果参数描述里包含装配关系可以自动生成装配体导出 STEP 装配或者 URDF 多连杆结构。这个复杂度高一个量级但价值也大。还有一个方向是和版本控制结合。参数文件用 Git 管理每次改动都有记录生成的模型也跟着版本走。这样设计迭代可追溯比在 CAD 软件里手动改靠谱得多。我自己在实际操作中的体会是text-to-cad 这套东西的价值不在于“炫技”而在于把确定性的事情交给代码把不确定的事情留给人。参数化变体、批量导出、格式转换这些代码比人快也比人稳而判断哪个形状合理、哪个尺寸该取多少还是得靠工程师的经验。把这条边界划清楚工具才能真正帮上忙。