build123d 0.11.1 建模模式精要:scientific-agent-skills 中 lab-hardware-cad 技能的几何编程实战指南
【免费下载链接】scientific-agent-skillsTurn any AI agent into an AI Scientist. The #1 Agent Skills library for science, used by 190,000+ scientists worldwide. 165 ready-to-use validated skills plus 100+ scientific databases covering biology, chemistry, medicine, and drug discovery. Compatible with Cursor, Claude Code, Codex, Pi, Antigravity, and the open Agent Skills standard.项目地址: https://gitcode.com/GitHub_Trending/cl/scientific-agent-skills
本指南是 lab-hardware-cad 技能配套的 build123d 0.11.1 API Cookbook 的系统化展开,面向"用参数化 Python 源码设计实验室硬件并导出可制造文件"这一核心工作流。文章以 build123d-patterns.md 为骨架,结合仓库内 gen.py、check.py、_common.py 等脚本源码与 standards.json 标准数据库,逐一拆解接口声明、几何量规、定位对齐、孔洞沉孔、选择器圆角、草图拉伸、多格式导出等模式,并总结 12 类真实踩坑点。读完你可以写出符合本技能契约、可被--param覆盖、可被机器校验的 build123d 模型,并理解每个模式背后的实现原理。
文中所有代码片段均针对 build123d 0.11.1 + Python 3.12 验证通过,与 SKILL.md 声明的一级依赖(build123d==0.11.1、matplotlib>=3.8)保持一致。
模型文件契约:gen.py 眼中的一个"好模型"
在进入具体 API 之前,先明确本技能对模型文件的硬性契约(定义于 SKILL.md 的工作流第 4 步,并由 gen.py 强制):
- 每个可被用户改动的尺寸都必须是模块级命名常量,且单位写进名字:
bore_d_mm、wall_t_mm、post_h_mm。除 0、1、2 外,函数体内不允许出现裸数字。 - 暴露
build() -> Part,gen.py会调用它。 - 参数分成
INTERFACE块(由标准固定的尺寸,标注标准 ID)和DESIGN块(可自由选择的尺寸)。 - 暴露
interfaces()函数声明"本零件必须配合的标准尺寸"(第 4 步机器可校验的基础),以及checks()函数声明"从实体上实测的通过/不通过量规"(第 5 步构建门禁)。 - 工艺(Process)、材料(Material)、每个接口的来源写在模块 docstring 里。
gen.py 的命令行形态决定了这些约束的由来:
python scripts/gen.py carrier_model.py --outdir out/ python scripts/gen.py carrier_model.py --outdir out/ --param wall_t_mm=4.0 python scripts/gen.py plate_model.py --outdir out/ --dxf # 2D 激光切割轮廓其中--param支持重复传入KEY=VALUE,解析逻辑在 _common.py:值会被自动收窄为bool/int/float/str,随后在import_model()中通过setattr(module, key, value)直接改写模块属性(_common.py),并且在导入时即应用——这正是"派生尺寸必须放进函数"这一铁律的根源,下文会展开。
Builder 模式还是代数模式?
build123d 提供两套等价 API,本技能推荐混合使用,但同一个build()内绝不混用:
# Builder 模式:上下文管理器收集操作,mode= 控制布尔运算 with BuildPart() as ex: Box(80.0, 60.0, 10.0) Cylinder(radius=11.0, height=10.0, mode=Mode.SUBTRACT) part = ex.part # 代数模式:普通对象与运算符 part = Box(80.0, 60.0, 10.0) - Cylinder(radius=11.0, height=10.0)本技能中的零件一律使用 Builder 模式。理由有三,都与技能工作流直接相关:
- 选择器(
ex.edges()、ex.faces())从 builder 上下文读取十分自然,这是倒角(fillet)和在已找到的面上放置特征(如把沉孔放在顶面)的必要前提; gen.py、check.py、snapshot.py都约定调用build()返回builder.part(见 _common.py 的run_model),而 Builder 对象本身不是 Part;- 代数模式更适合短小、纯构造性的形状,作为理解布尔的速记即可。
声明接口:让内部特征可被机器校验
实验室硬件的多数接口是内部特征——沉孔(pocket)、孔(bore)、槽(slot),它们不会出现在零件外包围盒(bounding box)上。因此check.py fit无法通过测量 STEP 找到它们;而手工把数字再敲进--value,恰恰重新引入了本技能要消灭的转写错误。interfaces()声明就是为了闭合这个回路:gen.py 在每次生成时把声明写进 manifest,check.py 的interfaces子命令逐一校验。
契约模板与字段语义
def interfaces() -> list[dict]: return [ {"feature": "cage rod bore spacing", "standard": "cage-system-30mm", "dimension": "rod_spacing", "value": rod_spacing_mm, "intent": "match"}, {"feature": "cage rod bore diameter", "standard": "cage-system-30mm", "dimension": "rod_diameter", "value": rod_bore_d_mm, "intent": "envelope", "clearance": 0.4}, ]| 键 | 含义 |
|---|---|
standard | 来自check.py standards --list的标准 ID |
dimension | 该标准内部的一个尺寸名 |
value | 本模型算出的数字,单位 mm |
feature | 校验输出中的人类可读标签(默认取尺寸名) |
intent | match:本零件自身必须符合标准;envelope:该特征必须能容纳任意合规零件(默认match) |
clearance | 期望的总间隙,单位 mm,两侧合计(默认 0) |
_common.py 的normalise_interfaces()给出了校验细则:standard、dimension、value三键必填且value必须可转浮点数;intent只接受match或envelope;clearance默认 0.0。而 check.py 的_evaluate()揭示了两种 intent 的数学差异:
match:以名义值为中心的对称带,被 clearance 加宽(绝不平移)——low = nominal - tol_minus - offset,high = nominal + tol_plus + offset;envelope:单侧最小下限low = nominal + tol_plus + offset——即最大实体条件(MMC,名义值 + 正公差)加 clearance,actual >= low即通过。若按名义值设计容纳特征,只会适配合规零件中较小的那一半。
offset < 0会被直接拒绝(check.py):负 clearance 相当于声明自己移动验收带、认证一个不合格值,这是该技能明确禁止的编码行为。
用函数而不是静态列表
# 错误:--param plate_tol_mm=0 静默地让 pocket_l_mm 停留在旧值 pocket_l_mm = plate_l_mm + plate_tol_mm + 2 * pocket_clearance_mm # 正确:每次调用都重算,覆盖才能生效 def pocket_l_mm() -> float: return plate_l_mm + plate_tol_mm + 2 * pocket_clearance_mm原因在 gen.py:模块级的INTERFACES = [...]列表在 import 时即求值——早于--param应用——任何由被覆盖参数派生的值都会被错误记录。gen.py一旦发现"静态 INTERFACES 列表 +--param"的组合就打印警告。同样的原则适用于几何本身:所有派生尺寸都在build()或辅助函数内部计算,绝不在模块级。
声明几何检查:对着实体实测的 go/no-go 量规
interfaces()只把声明数字与标准数据库比较,从不触碰实体。checks()是它的实测对应物:一系列布尔量规,通过"与build()实际产出的零件做布尔交集"来求值。gen.py 每次生成都运行这些量规、逐条打印 PASS/FAIL、把结果写进 manifest,并在任一失败时以非零码退出——违反自身声明几何的零件永远不会静默变成交付物。check.py 的geometry子命令则对已导出的 STEP(权威工件)重跑同一组量规。
核心原则:请求中的每个几何要求都映射为一个条目——
- 必须穿过的东西(螺丝、光束、探针)→
clear区域; - 必须落入空腔的东西(放入 pocket 的板)→ 按配合零件最大实体条件尺寸的
clearbox; - 必须保留的东西(脊、台阶、螺丝座)→
material区域; - 用户声明的尺寸上限 →
bbox_*边界。
def checks() -> list[dict]: top = plate_t_mm / 2 return [ # clear 区域:不得有材料侵入(螺丝轴,贯穿零件) {"feature": "M6 screws pass all four bores", "clear": {"cylinder": 6.0, "axis": "z", "at": bolt_xy()}}, # 带显式区间的净空(离台面 15 mm 高处的光束走廊) {"feature": "beam clear at 15 mm above the bench", "clear": {"cylinder": 5.0, "axis": "x", "at": [(0.0, 15.0)]}}, # 量规零件必须落入 pocket:MMC 下的配合件 {"feature": "SLAS plate at MMC drops into the pocket", "clear": {"box": (128.01, 85.73, pocket_depth_mm()), "at": [(0.0, 0.0, floor_t_mm + pocket_depth_mm() / 2)]}}, # 沉孔真的得是沉孔:凹槽顶部敞开、座面存在。 # 第二条才是抓住"凹槽贯穿到底"的关键。 {"feature": "counterbore recess open at the top", "clear": {"cylinder": cbore_d_mm - 0.2, "axis": "z", "at": bolt_xy(), "span": (top - cbore_depth_mm + 0.1, top + 0.1)}}, {"feature": "screw seat present below the recess", "material": {"cylinder": cbore_d_mm - 0.2, "axis": "z", "at": bolt_xy(), "span": (-top + 0.1, top - cbore_depth_mm - 0.1)}, "min_mm3": 50.0}, # 用户声明的硬性上限,从实体实测 {"feature": "clears the objective turret", "bbox_z": {"max": 15.0}}, ]量规模式语义
| 键 | 含义 |
|---|---|
clear/material | 该区域必须不含材料 / 必须含材料 |
{"cylinder": DIA, "axis": "x"\|"y"\|"z", "at": [(a, b), ...], "span": (lo, hi)} | at是垂直于轴平面上的 2D 坐标——轴 z:(x, y);轴 x:(y, z);轴 y:(x, z)。省略span则贯穿整个零件 |
{"box": (dx, dy, dz), "at": [(x, y, z), ...]} | 轴对齐盒形量规,以各位置为球心(中心点) |
tol_mm3/min_mm3 | 每个位置的通过阈值(clear 侵入体积上限 / material 材料体积下限,默认均 0.01) |
bbox_x…bbox_z、bbox_min/mid/max | 实测包围盒的{"min": mm, "max": mm}边界 |
区域校验规则在 _common.py 的_normalise_region/normalise_checks中逐项强制:cylinder 直径必须 > 0、轴只能是 x/y/z、at坐标数量与轴向匹配、box 三边必须 > 0;每个条目必须恰好拥有clear、material或一个bbox_*测量之一,否则抛错。实测求值在evaluate_checks()(_common.py):对每个位置实体化量规 solid(圆柱沿轴、at平面定位;盒在 3D 中心),与零件做布尔交集并累计体积——clear要求每位置侵入体积 ≤tol_mm3,material要求每位置材料体积 ≥min_mm3。注意intersection_volume()(_common.py)对内核返回None、空Compound或无.volume的ShapeList一律按 0 处理,这正是"相切或分离实体的布尔结果是空的而非错误"这一坑的工程化应对。
量规尺寸从要求自身的数字来
只有当要求是关系型的(凹槽位于座面之上)才用与几何相同的命名常量去度量规;当要求是绝对型的——配合件的 MMC、用户的高度限制、光束位置——就用量规自身的要求数字,这样错误的参数无法把量规缩小到与错误几何"匹配"。
不改模型的一次性探针与钻孔普查
对"不改模型的一次性问题",check.py 的probe子命令直接从命令行运行单个量规:
python scripts/check.py probe out/part.step --cyl 6.6 --at 37.5,37.5 --at -37.5,37.5 python scripts/check.py probe out/part.step --box 40,40,5 --at 0,0,7 --expect material而bores子命令输出每个圆柱面的普查(直径、轴、位置、跨度、扫掠角),用于与模型意图对账(_common.py 的cylinder_census):约 360° 扫掠的圆柱面是孔/轴,约 90° 扫掠的是边倒角,沉孔则是沿轴堆叠的两个同心全扫掠面、半径不同。这正是 SKILL.md 第 6 步"渲染图分辨不出 0.3 mm 特征时"的仪器化替代。
定位:螺栓阵列与微孔板井格
Locations将其内部创建的对象整体放置,是螺栓阵列的主力:
with Locations((10.0, 0.0), (-10.0, 0.0)): # 当前平面上的两个位置 Hole(radius=3.3) with Locations((0.0, 0.0, floor_t_mm)): # z 方向偏移 Box(10.0, 10.0, 5.0, mode=Mode.SUBTRACT) with GridLocations(9.0, 9.0, 12, 8): # x 间距, y 间距, x 数量, y 数量 Hole(radius=1.5)GridLocations把网格居中于原点;而微孔板井格是按板角定标的,所以要计算绝对坐标再交给Locations:
a1_x_mm, a1_y_mm, pitch_mm = 14.38, 11.24, 9.0 # slas-well-positions-96 origin_x = -plate_l_mm / 2 origin_y = plate_w_mm / 2 wells = [ (origin_x + a1_x_mm + pitch_mm * col, origin_y - a1_y_mm - pitch_mm * row) for row in range(8) for col in range(12) ] with Locations(*wells): Hole(radius=well_clear_d_mm / 2)这些数字的来源可在 standards.json 中核实:slas-well-positions-96定义的well_pitch名义值 9.0 mm、a1_offset_x14.38 mm(左外沿到第 1 列中心)、a1_offset_y11.24 mm(顶外沿到 A 行中心),且每个井心须落在名义位置的 0.70 mm 位置公差带内——本技能的命令行入口正是check.py standards --show slas-well-positions-96。微孔板的外轮廓则来自slas-microplate-footprint(footprint_length127.76 mm、footprint_width85.48 mm,±0.25 mm,角区 12.7 mm 内测量,侧边中点处放宽到 ±0.5 mm)。
对齐:经典"切穿地板" bug
默认情况下对象以原点为中心。align移动基准点,通常正是你想要的——让 pocket 从底面开始:
Box(x, y, z, align=(Align.CENTER, Align.CENTER, Align.MIN)) # 坐落在 z = 0 Box(x, y, z, align=(Align.MIN, Align.MIN, Align.MIN)) # 角点在原点这个搞错就是经典的"pocket 切穿地板"故障——snapshot.py 生成的多视图 PNG 正是为捕捉这类问题而存在的(SKILL.md 第 6 步是强制步骤,不因确定性检查通过而豁免)。SKILL.md 的示例模型carrier_model.py中Box(..., align=(Align.CENTER, Align.CENTER, Align.MIN))配合Locations((0, 0, floor_t_mm))的Mode.SUBTRACTbox,就是"对齐 + 抬升"的教科书组合:主体坐底、掏空从底板顶面开始。
孔:沉孔的工作平面陷阱
Hole贯穿整个零件;CounterBoreHole与CounterSinkHole增加头部凹槽。
CounterBoreHole从其所放置的工作平面向下切,凹槽就在该平面上。在居中的Box上,默认工作平面是零件的中间高度,所以 2 元组位置会把螺丝座埋进板内——或者在薄板上让凹槽吞掉整个顶部,留下一个螺丝头直接掉下去的直通孔。正确的做法是放在顶面上(或给位置一个显式的顶部 z):
with BuildPart() as plate: Box(60.0, 60.0, 10.0) # 覆盖 z = -5 .. +5 top = plate.faces().sort_by(Axis.Z)[-1] with Locations(top): with Locations((20.0, 20.0)): CounterBoreHole(radius=6.6 / 2, counter_bore_radius=11.0 / 2, counter_bore_depth=6.5)counter_bore_depth要从螺丝头高度取,而不是凭习惯:M6 内六角圆柱头螺丝头高 6.0 mm,1/4-20 头高 6.35 mm(后者记录在 standards.json 的optical-breadboard-metric的screw_head_height中;counterbore_dia11.0 mm 对应 M6 沉头孔径)。4 mm 的沉孔会让两种头都高出 2 mm——别把这种叫齐平。生成后在快照(或剖面)里确认凹槽位于顶面且座面台阶存在:这两种失败模式都能让is_valid和包围盒原样通过。
另外牢记打印孔会偏小(参见 fabrication-limits.md:FDM 与 SLA 的 6.0 mm 建模孔通常实测不足 6.0 mm),功能孔要么放大要么计划铰孔。standards.json 中optical-breadboard-metric的clearance_hole_normal(6.6 mm)正是"打印件优先"的数字。
选择器:安全地倒角与圆角
选择器用于找出要倒角、圆角或在其上构建的边与面。本技能需要的三个:
part.edges().filter_by(Axis.Z) # 保留平行于 Z 的边(竖直棱角) part.edges().group_by(Axis.Z)[-1] # 最高 Z 的那一组(顶部边) part.faces().sort_by(Axis.Z)[-1] # 单个最高面 part.edges().filter_by(GeomType.CIRCLE) # 只留圆形边filter_by保留所有匹配项;group_by按键分区成列表,[-1]是最后一组、[0]是第一组;sort_by对单项排序。
with BuildPart() as ex: Box(80.0, 60.0, 10.0) chamfer(ex.edges().group_by(Axis.Z)[-1], length=4.0) # 顶面边倒角 fillet(ex.edges().filter_by(Axis.Z), radius=5.0) # 竖直棱角圆角这些宽泛选择器只在零件仍是纯长方体时安全。一旦零件有了 pocket、孔、槽或微结构,filter_by(Axis.Z)和group_by(Axis.Z)[-1]也会选中那些特征的边,圆角要么抛内核错误(Failed creating a fillet、BRep_API: command not done),要么——更糟——成功并悄悄吃掉一面墙或一条 0.3 mm 的脊。两种都在实践中发生过。因此:
- 在添加内部特征之前对外部主体圆角/倒角,或用位置、长度、
GeomType刻意过滤选择,只留下目标边; - 附近几何紧张时用
part.max_fillet(edges)约束半径——它返回内核在该边集上实际能构建的最大半径; - 每个圆角/倒角半径都是命名参数,内核失败时回退数值而不是对抗选择器;
- 然后检查快照:被吃掉的特性在图片里一目了然,在
is_valid里隐形。
草图后拉伸:非图元轮廓与激光切割 DXF
轮廓不是图元时,画草图再拉伸:
with BuildPart() as bracket: with BuildSketch() as profile: Rectangle(40.0, 20.0) with Locations((15.0, 0.0)): Circle(radius=4.0, mode=Mode.SUBTRACT) extrude(amount=6.0)这也是通向激光切割 DXF 的路径——草图就是切割轮廓。gen.py 的_export_dxf就是这条路径的成品实现:默认在零件中间高度切片(因为坐落在构建板上的零件在 z=0 处只有退化面),把剖面平移回 z=0(否则 DXF 写入器会对非平面形状告警),再把切割几何放到命名层CUT(层色ColorIndex.RED),因为激光加工商把功率/速度映射到层或颜色上。
导出:STEP 是唯一权威
gen.py负责这些,但作为参考:
export_step(part, "part.step", unit=Unit.MM) # 权威工件 export_stl(part, "part.stl", tolerance=1e-3, angular_tolerance=0.1) # 激光切割的 2D 剖面。section() 是模块级操作,不是形状的方法—— # part.section(...) 会抛 AttributeError。 from build123d.exporters import ColorIndex # 不在 `from build123d import *` 里 profile = section(part, Plane.XY.offset(z_mm), mode=Mode.PRIVATE) profile = profile.moved(Location((0, 0, -z_mm))) # 移回 z = 0,否则 DXF 写入器 # 会警告非平面形状 exporter = ExportDXF(unit=Unit.MM) exporter.add_layer("CUT", color=ColorIndex.RED) # 激光加工商把功率/速度映射到层 exporter.add_shape(profile, layer="CUT") exporter.write("part.dxf")切面要穿过材料,而不是在 z=0:坐落在构建板上的零件在那里只有退化面。gen.py --dxf默认取零件中间高度,可用--dxf-z覆盖(gen.py 还会在只给了--dxf-z而没给--dxf时直接报错)。导出的其他参数化入口:--tolerance(STL 线性偏差,默认 0.001)、--angular-tolerance(角度偏差,默认 0.1)、--no-stl。
STEP 保留精确的 BREP 几何;STL 是三角化近似。始终以 STEP 为真值源,网格从它重新生成,绝不反向。
在代码里测量
在模型内部断言接口时很有用:
bbox = part.bounding_box() print(bbox.size.X, bbox.size.Y, bbox.size.Z) print(part.volume, part.area) print(part.is_valid) # 0.11.1 里是属性,不是方法 print(part.center(CenterOf.MASS))is_valid是属性而非方法,这是与旧版本及部分文档的真实差异,访问时不要带括号。check.py 的facts子命令正是这些测量的工程化集合(is_valid、包围盒、体积、面积、质心、实体数,且is_valid: false时以非零码退出);_common.py 的_is_valid甚至对"属性 vs 方法"两种形态做了兼容。
易踩坑清单:12 个真实教训
is_valid是属性。part.is_valid()抛TypeError: 'bool' object is not callable。section()是模块级操作,不是方法。part.section(Plane.XY)抛AttributeError。应调用section(part, plane, mode=Mode.PRIVATE)。intersect()返回没有.volume的ShapeList;&运算符返回有.volume的Solid。check.py 的clearance子命令两者都处理(先试交集体积,再退化为最小距离)。- 永远不要把脚本命名为
inspect.py放在会进入sys.path的目录里。它会遮蔽标准库inspect,破坏typing_extensions,进而连 build123d 一起搞坏。这正是本技能内置脚本叫check.py的原因。 - Builder 对象不是 Part。返回
builder.part,而不是 builder。 Mode.SUBTRACT需要已存在的体。在空上下文里做减法会静默地什么都不发生。- 扫掠或拉伸的轮廓默认以其路径/平面为中心,除非你对齐。沿曲面路径扫掠
Rectangle(w, h)会让半个轮廓沉到曲面下方——一条"0.3 mm 的脊"其实是 0.15 mm 突出。传align=(并在轮廓平面上给显式x_dir),让轮廓落在你以为的位置,然后测量结果。 Curve没有.length。对边求和:sum(e.length for e in curve.edges())。- 相切或分离实体的布尔结果是空的,不是错误。取决于路径你会得到
None、空Compound或无.volume的ShapeList——在任何干涉检查里读取.volume之前先做守卫(_common.py 的处理即是范例)。 ColorIndex和LineType住在build123d.exporters,不在顶层命名空间;from build123d import *带不进它们,add_layer(color=1)会失败。- OpenCascade 内核会抛多种异常类型。在布尔运算周围宽泛捕获并报告失败,而不是让 traceback 逃逸(gen.py 与 _common.py 均如此处理)。
- 孔洞建模后的沉孔/倒角陷阱:见上文"选择器"与"孔"两节——特征选择错误与工作平面错误都既过得了
is_valid、也过得了包围盒检查,只能靠快照与check.py bores普查来发现。
配套材料与下一步
- SKILL.md:完整工作流(8 步:路由到器件族 → 建立接口尺寸 → 先选工艺 → 参数化建模 → 生成与检查 → 快照目检 → 源码修复 → 制造前报告),以及全部命令的速查表。
- fabrication-limits.md:工艺公差与最小特征、配合间隙表(自由滑动 FDM 0.40 / SLA 0.20 / CNC 0.10 mm 每侧)、螺纹与热熔嵌件、材料热/化学/生物相容性。
- standards.json:11 个内置标准(
slas-microplate-footprint、slas-microplate-height、slas-microplate-flange、slas-well-positions-96/384/1536、cuvette-standard-10mm、optical-breadboard-metric/imperial、cage-system-30mm、sm1-lens-tube-thread),全部可由check.py standards --list / --show浏览。 - gen.py / check.py / _common.py / snapshot.py:本文引用的所有契约、校验与渲染逻辑的源码所在。
- 器件族参考:microfluidics.md、optomechanics.md、labware-adapters.md、behavior-rigs.md,以及制造前清单 validation.md。
编写模型时请记住技能的底线:模型文件是被执行的而非被解析的——只运行本会话编写或用户可信来源提供的模型;界面尺寸永远查标准文件或问用户,绝不凭记忆写;STEP 是权威工件,绝不手改导出文件,也绝不从网格逆向重建。
【免费下载链接】scientific-agent-skillsTurn any AI agent into an AI Scientist. The #1 Agent Skills library for science, used by 190,000+ scientists worldwide. 165 ready-to-use validated skills plus 100+ scientific databases covering biology, chemistry, medicine, and drug discovery. Compatible with Cursor, Claude Code, Codex, Pi, Antigravity, and the open Agent Skills standard.项目地址: https://gitcode.com/GitHub_Trending/cl/scientific-agent-skills
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考