🔶 基准测试:CadQuery 与 OpenSCAD,谁更适合 Agent 驱动的 CAD 工作
英文标题:🔶 Benchmark: CadQuery vs. OpenSCAD for agentic CAD work
ModelRift 使用 Claude Opus 5 通过 Claude Code,对 CadQuery 和 OpenSCAD 完成三项无人值守的可打印零件生成任务进行了对照测试。两种工具最终都产出了通过独立 STL 解析器验证的干净网格,但 OpenSCAD 的几何重算速度明显更快,而 CadQuery 在参数关联、几何查询和断言验证方面更有优势。测试显示,CadQuery 倾向于较早以异常方式失败,OpenSCAD 则更可能静默生成错误几何,因此两者自身的成功或有效性报告都不能替代外部网格验证。对 Agentic CAD 而言,作者认为渲染检查不足以发现关键缺陷,体积、干涉、角度和网格拓扑等可量化验证应成为自动化 QA 流程的核心。
57.5 KB
60.6 KB
58.2 KB
62.5 KB
47.5 KB
46.6 KB
64.8 KB
79.9 KB
112.4 KB
31.4 KB
中文
ModelRift 平台上的每个模型都生成 OpenSCAD 代码。这个选择值得不时重新检验,因此我们针对代码优先 CAD 最可信的替代方案——构建于 OpenCascade B-rep 内核之上的 Python 库 CadQuery——进行了受控对比。
问题很聚焦:在无人监督的情况下,哪一种工具能由 AI Agent 驱动,生成正确、可打印且可用的零件?至于哪一种手写起来更愉悦,并不在本次讨论范围内。
6 个 Agent、3 项任务、2 种工具。随后,每个生成的 STL 都由一个不信任任何一种工具的解析器进行检查。
6 个零件最终都可以打印。能力反而成了答案中最乏味的部分。两者真正的差异,在于它们如何失败。

任务集中最困难的一项,两种工具均已完成。左侧为 OpenSCAD,右侧为 CadQuery。
测试设置
全部 6 次运行均由 Claude Opus 5(100 万上下文)通过 Claude Code 驱动。每个测试单元均作为独立的通用子 Agent 运行,继承相同模型,彼此之间没有通信。每个单元一个 Agent,因此下文中的部分差异来自 Agent 方差,而非工具差异。工具版本为 Python 3.14 上的 CadQuery 2.8.0 和 OpenSCAD 2026.06.12,均运行于一台 M 系列 Mac 上。
OpenSCAD 一侧使用我们自行开发的 openscad-skill,即我们公开发布并在内部使用的 Agent skill。它涵盖文件与版本命名、渲染—检查—修复 QA 循环、用于 CLI 预览的相机预设、截面调试以及 Customizer 语法。
对于 CadQuery,我们逐项迁移了这套 skill,保持相同结构,只替换不存在 OpenSCAD 等价物的部分。CadQuery 没有 CLI 渲染器,因此迁移时需要为其编写一个小型离屏渲染器,并以 B-rep 有效性指标取代 CSG 状态行。随后,两套文件都采用相同的 3D 打印设计规则、壁厚、间隙和悬垂要求,因此没有任何一方获得另一方缺少的建议。OpenSCAD 文件大小为 12.4 KB,CadQuery 文件为 13.1 KB。
仍保留了一项不对称:CadQuery 文件带有 API 速查表,而 OpenSCAD 文件不需要,因为模型已经非常熟悉 OpenSCAD 语法。这会帮助 CadQuery 解决语法问题,但对几何问题没有帮助。
每个 Agent 最多可创建 12 个版本,被告知绝不能伪造成功,并且必须报告每一项失败及其逐字错误文本。它们在无人值守状态下运行。
我们不相信 Agent 对任何结果的自我报告。每个最终 STL 都通过一个直接读取文件的解析器,报告三角形数量、包围盒、体积、水密性、非流形边和边界边、翻转面以及连通组件数。出于后文的原因,这比我们预想的更重要。
三项任务
同一任务中的两个 Agent 获得完全相同的文本,不含针对特定工具的提示。
最简单的 T1 是一个壁挂式搁板 L 形支架:两块以 90 度连接、厚度为 4 的板,两个三角形加强筋,两个适用于平头螺钉、角度为 90 度且头部直径为 9 的沉头孔,两个普通孔,外侧垂直角为 R3,内角圆角为 R4,并且应当无需支撑即可打印。
T2 提高了难度:为一块 50 x 26 PCB 设计一个由两部分组成的卡扣式外壳。壁厚为 2,腔体每侧间隙为 0.4,具有四个 M2 柱、一个 9.5 x 3.5 的 USB-C 开孔、一个独立上盖;上盖带有周边唇缘,间隙为 0.2,并配备可工作的卡扣以及 5 个通风槽。这两个部件必须能够实际配合。
T3 是最困难的任务:一个 M24x2 螺纹倒钩软管转接头,六角法兰对边宽度为 30,具有一段实际的、长度为 12 的螺旋螺纹;长度为 25 的倒钩部分有 3 道倒钩,适配内径为 12 的软管,并带有一个 8 的贯穿通道。规格明确禁止叠加圆环。螺纹必须是真正的螺旋几何。
结果
| T1 OpenSCAD | T1 CadQuery | T2 OpenSCAD | T2 CadQuery | T3 OpenSCAD | T3 CadQuery |
|---|---|---|---|---|---|
| 版本数 | 2 | 3 | 8 | 5 | 1 |
| 代码行数 | 103 | 135 | 220 | 266 | 150 |
| 工具抛出的错误 | 1 | 4 | 0 | 0 | 1 |
| 静默错误几何 | 0 | 0 | 5 | 3 | 0 |
| Agent 墙钟时间 | 461 s | 562 s | 1058 s | 909 s | 542 s |
| Agent token 数 | 77 k | 89 k | 138 k | 139 k | 82 k |
| 几何重算 | 12 ms | 1.56 s | 16 ms | 1.93 s | 43 ms |
| 最终 STL 结论 | clean | clean | clean | clean | clean |
三项任务汇总如下:
| OpenSCAD | CadQuery |
|---|---|
| 版本数 | 11 |
| 代码行数 | 473 |
| 工具抛出的错误 | 2 |
| 静默错误几何 | 5 |
| Agent 墙钟时间 | 2061 s |
| Agent token 数 | 297 k |
“干净(clean)”意味着:水密、单一连通组件、零非流形边或边界边、位于 z = 0 平面上;这些结果来自直接测量文件,而不是由写出文件的工具自行报告。
迭代次数完全相同,双方均为 11 个版本。整体工作量形态也相同。不同之处在于,每个 Agent 遇到的问题类型。
T1:简单支架

两者均正确。图中从内角展示,以便看见加强筋。可见差异在于规格解释:规格没有规定加强筋的尺寸和内缩距离。
为四个外角倒圆,清楚地区分了两种世界模型。OpenSCAD 对一个 2D 轮廓进行圆角处理后再拉伸,因此该操作不关心实体是如何装配的:
module corner_mask() {
translate([0, 0, -eps]) linear_extrude(vert_h + 2*eps)
offset(r = corner_r) offset(delta = -corner_r) square([plate_w, horiz_d]);
}
CadQuery 则必须先指定这四条边,而命名边正是困难之处:
result = result.edges("|Z").edges(
BoxSelector((-WIDTH, -EPS, -EPS), (WIDTH, EPS, V_HEIGHT + EPS))
+ BoxSelector((-WIDTH, H_DEPTH - EPS, -EPS), (WIDTH, H_DEPTH + EPS, V_HEIGHT + EPS))
).fillet(R_OUTER)
OpenSCAD 第一次尝试便编译出正确几何,并在两个版本内完成。CadQuery 大约用三分之一的运行时间处理一个单独错误:.fillet(3.0) 因 BRep_API: command not done 失败;这条消息既未指出边,也未指出半径。Agent 必须手动二分排查,才发现原因是算术问题:两个 R3 圆角无法容纳在 4 mm 厚的壁中。
T2:必须配合的两部分

两个 Agent 都从间隙叠加关系推导出了相同的 54.8 x 30.8 x 25.0 mm 盒体。
这是 CadQuery 的参数链体现价值的地方。改变唇缘深度时,边框、平板、槽、沟槽和倒钩会同步移动,因为每一个尺寸都是推导得出,而非重复输入两次。这类尺寸链与一个优秀的参数化 UI 向用户呈现的内容相同,我们曾在 构建更好的 OpenSCAD customizer 一文中讨论过这一点。CadQuery 没有与 Customizer 对应的功能,因此常量块就是完整的参数接口。
更有意思的是每一方如何检查配合关系。OpenSCAD 输出数字,供人阅读:
echo(str("cavity LxW = ", cav_l, " x ", cav_w, " (clearance/side ", pcb_clr, ")"));
CadQuery 使用断言;当断言为假时,构建会停止:
assert BOX.val().intersect(LID.val()).Volume() < 1e-6, "box and lid interfere"
只有在有人阅读时,echo 才有帮助。assert 会自行使构建失败。对于无人值守生成而言,这一差距几乎就是全部故事。
OpenSCAD 需要 8 个版本,而 CadQuery 为 5 个;OpenSCAD 的 8 个版本中有 3 个用于布尔运算卫生问题而非设计:清理相切面和共面产生的碎片面。
T3:真正的螺纹
最难的任务带来了最大的意外。OpenSCAD 在一个版本内完成,首次编译即正确,耗时 43 ms,且没有使用库。
OpenSCAD 没有扫掠操作,因此 Agent 将螺旋线写成原始的顶点和面算术:一个四点 ISO 截面、每圈 96 个截面,以单个 polyhedron 输出。这段代码完全没有护栏。若绕序错误,工具不会报告任何问题。
module helical_thread(turns, z0) {
prof = [ [r_in, -flank_hz], [r_maj, -crest_hz],
[r_maj, crest_hz], [r_in, flank_hz] ];
n = round(turns * STEPS);
pts = [ for (i = [0:n]) let(a = i * 360 / STEPS,
zo = z0 + i * thread_pitch / STEPS)
for (j = [0:3])
[ prof[j][0] * cos(a), prof[j][0] * sin(a), prof[j][1] + zo ] ];
fcs = concat(
[ [3, 2, 1, 0] ],
[ for (i = [0:n-1]) for (j = [0:3]) let(k = (j + 1) % 4)
[ 4*i + j, 4*i + k, 4*(i+1) + k, 4*(i+1) + j ] ],
[ [4*n + 0, 4*n + 1, 4*n + 2, 4*n + 3] ]
);
polyhedron(points = pts, faces = fcs, convexity = 8);
}
Agent 在写下任何一行代码之前,还预先规避了经典螺纹陷阱:ISO 齿形恰好跨越一个螺距,因此连续的牙底平面会落在同一平面上,导致并集运算出错。它让扫掠截面沉入小径以下,使螺旋线穿过芯体而不是仅与之接触。这就是布尔运算没有成为问题的原因。
CadQuery 用 6 行易读代码表达了相同的几何,这部分首次尝试就成功了:
helix = cq.Wire.makeHelix(pitch=THREAD_P, height=h, radius=R_ROOT, center=(0, 0, z0))
prof = (cq.Workplane("XZ", origin=(0, 0, z0)).center(R_ROOT, 0)
.polyline(thread_profile_points()).close())
ridge = prof.sweep(path, isFrenet=True)
core = cq.Workplane("XY", origin=(0, 0, z0)).circle(R_ROOT).extrude(h)
rod = core.union(ridge)
失败发生在最后一行,这一结果改变了我们对 QA 循环的看法。

CadQuery T3 的版本 1 位于左侧。螺纹段仅由悬浮的螺旋圈组成。
由于螺纹根部恰好位于芯体半径上,union() 静默丢弃了芯部圆柱。沿螺旋曲线的精确相切,OCCT 一声不响地丢弃了一个实体。从外部看,该零件完美无缺。Agent 查看了 4 张渲染图,却没有注意到问题。发现它的是体积测量:实际为 7065 mm³,而预期为 10323 mm³。
更糟的是,损坏的零件报告 valid=True 和 solids=1。将布尔容差提高至 1e-3 后,生成了一个负体积实体,而它同样报告 valid=True。
OpenSCAD 在此也并非无辜。T2 中,它的 Manifold 后端对一个包含 4 条非流形边和 60 个零面积三角形的 STL 认证为 Status: NoError。两种工具的自我报告都不是最终裁决,这正是我们自行解析每一个网格的原因。

两个完成的螺纹。左侧牙侧的牙顶与右侧牙侧的牙顶错开半个螺距,这是单头真实螺旋线的特征,也是区分真正螺纹和叠加圆环的检查方法。
下载零件
以下是 8 个最终网格,它们与 Agent 导出的结果完全一致,我们未进行任何清理或修复。它们是二进制 STL,单位为毫米,均已按打印方向放置,即零件位于 z = 0 平面上。
这 8 个网格中的每一个都水密、仅有一个连通组件、零非流形边、零边界边和零翻转面。间隙假设使用 0.4 喷嘴的 FDM 打印,因此每一对 T2 部件在原样打印后都应能卡合。
三角形数量值得一看,而且并不始终偏向任一工具。CadQuery 的盒体三角形数量接近 OpenSCAD 盒体的 5 倍,而它的上盖却更少。这里的网格密度取决于每个 Agent 选择加入多少圆角细节,而非内核本身。
我们发现了什么
渲染没有发现任何重要问题
这是我们未曾预料的结果。在 6 次运行中,图像发现了粗大错误,例如腔体减法删除了 4 个安装柱,但没有发现任何细微问题。每一个足以毁掉一次打印的缺陷,都是通过数值发现的:体积、角度、干涉测试或应变计算。在 T3 中,OpenSCAD Agent 遇到了相反的问题,差点因为螺纹特写的渲染方式看起来不对而拒绝了正确几何。它自己的备注是:渲染无论朝哪个方向看,都不是充分证据。
失败模式如同镜像
CadQuery 的失败响亮且较早。它的错误消息较差,但异常会停止运行,而停止的模型无法意外交付。OpenSCAD 的失败无声且较晚:在 T2 中,它在大约 45 次调用期间没有报告错误或警告,却生成了被删除的安装柱、位置错误的槽,以及一个它刚刚认证为干净的损坏导出文件。对于无人值守生成,静默成功是代价更高的失败。
CadQuery 可以被查询,OpenSCAD 不行
CadQuery 可以回答关于自身几何的问题。T1 Agent 通过从 B-rep 中读取圆锥的半角,证明其沉头孔为 90 度且直径为 9,随后将规格写入每次构建都会重新运行的断言。OpenSCAD 无法查询几何,因此两个 OpenSCAD Agent 都独立编写了二进制 STL 解析器,以测量自己构建的内容;这些解析器的代码量大致与模型本身相当。它可行,但只能在导出后观察网格,永远无法观察设计本身。
速度偏向 OpenSCAD,但几乎无关紧要
几何重算速度快 30 到 100 倍:16 ms 对 1.9 s。在由模型推理主导的 Agent 循环中,这只是噪声。对于实时 customizer 或大规模参数扫描,这一点则会很重要。
OpenSCAD 渲染没有零件边的概念
在 CSG 之后,只剩下一堆三角面片,因此 --view=edges 绘制的是三角剖分,而不是轮廓线。CadQuery 的 B-rep 知道两个面实际在哪里相交。由于
English / 原文
ModelRift generates OpenSCAD for every model on the platform. That choice is worth re-testing occasionally, so we ran a controlled comparison against the most credible alternative for code-first CAD: CadQuery, a Python library on top of the OpenCascade B-rep kernel.
The question was narrow: which one can an AI agent drive to a correct, printable, functional part with nobody watching? How pleasant each is to write by hand did not come into it.
Six agents, three tasks, two tools. Every resulting STL was then checked by a parser that trusts neither tool.
All six parts came out printable. Capability turned out to be the boring part of the answer. Where the two diverge is in how they fail.

The hardest task in the set, solved by both tools. OpenSCAD on the left, CadQuery on the right.
Setup
All six runs were driven by Claude Opus 5 (1M context) through Claude Code. Each cell ran as a separate general-purpose subagent inheriting that same model, with no cross-talk between them. One agent per cell, so part of the spread below is agent variance rather than tool difference. Tool versions were CadQuery 2.8.0 on Python 3.14 and OpenSCAD 2026.06.12, both on an M-series Mac.
The OpenSCAD side ran on our own openscad-skill, the agent skill we publish and use in house. It covers file and version naming, the render-inspect-fix QA loop, camera presets for CLI previews, cross-section debugging, and Customizer syntax.
For CadQuery we ported that skill operation by operation, keeping the same structure and replacing only what has no OpenSCAD equivalent. CadQuery has no CLI renderer, so the port needed a small offscreen renderer written for it, plus B-rep validity metrics in place of the CSG status line. Both files then got the same 3D-printing design rules, wall thickness, clearances and overhangs, so neither side was handed advice the other lacked. Sizes landed at 12.4 KB for OpenSCAD and 13.1 KB for CadQuery.
One asymmetry survived: the CadQuery file carries an API cheatsheet the OpenSCAD file does not need, because the model already knows OpenSCAD syntax well. That helps CadQuery on syntax and does nothing for it on geometry.
Each agent was capped at 12 versions, told never to fake success, and required to report every failure with its verbatim error text. They ran unattended.
We did not take the agents’ word for anything. Every final STL went through a parser that reads the file directly and reports triangle count, bounding box, volume, watertightness, non-manifold and boundary edges, flipped faces, and connected components. That turned out to matter more than we expected, for reasons further down.
The three tasks
Both agents on a task got the same text, with no tool-specific hints.
The simple one, T1, was a wall-mounted shelf L-bracket: two plates at 90 degrees, 4 thick, two triangular gussets, two countersunk holes for flat-head screws at 90 degrees and 9 head diameter, two plain holes, R3 on the outer vertical corners, R4 fillet on the inner corner, printable without supports.
T2 raised the stakes to a two-part snap-fit enclosure for a 50 x 26 PCB. Walls 2, cavity clearance 0.4 per side, four M2 posts, a 9.5 x 3.5 USB-C cutout, a separate lid with a peripheral lip at 0.2 clearance and working snaps, five vent slots. The two parts had to actually fit.
T3 was the hard one: an M24x2 threaded hose-barb adapter with a hex flange 30 across flats, a real helical thread 12 long, a 25 long barb carrying three barbs for 12 ID hose, and an 8 through-channel. The spec explicitly banned stacked rings. The thread had to be true helical geometry.
Results
| T1 OpenSCAD| T1 CadQuery| T2 OpenSCAD| T2 CadQuery| T3 OpenSCAD| T3 CadQuery
---|---|---|---|---|---|---
Versions| 2| 3| 8| 5| 1| 3
Lines of code| 103| 135| 220| 266| 150| 172
Errors the tool raised| 1| 4| 0| 0| 1| 1
Silent wrong geometry| 0| 0| 5| 3| 0| 1
Agent wall-clock| 461 s| 562 s| 1058 s| 909 s| 542 s| 935 s
Agent tokens| 77 k| 89 k| 138 k| 139 k| 82 k| 119 k
Geometry recompute| 12 ms| 1.56 s| 16 ms| 1.93 s| 43 ms| 1.95 s
Final STL verdict| clean| clean| clean| clean| clean| clean
Summed across the three tasks:
| OpenSCAD| CadQuery
---|---|---
Versions| 11| 11
Lines of code| 473| 573
Errors the tool raised| 2| 5
Silent wrong geometry| 5| 4
Agent wall-clock| 2061 s| 2406 s
Agent tokens| 297 k| 347 k
“Clean” means watertight, one connected component, zero non-manifold or boundary edges, sitting on z = 0, measured from the file rather than reported by the tool that wrote it.
Iteration count came out identical at 11 versions each. So did the broad shape of the effort. What differs is the character of the problems each agent hit.
T1: the simple bracket

Both correct, shown from the inner corner so the gussets are visible. The visible difference is interpretation: the spec left gusset size and inset open.
Rounding the four outer corners separates the two models of the world cleanly. OpenSCAD rounds a 2D profile and extrudes it, so the operation does not care how the solid was assembled:
module corner_mask() {
translate([0, 0, -eps]) linear_extrude(vert_h + 2*eps)
offset(r = corner_r) offset(delta = -corner_r) square([plate_w, horiz_d]);
}
CadQuery has to name the four edges first, and naming is the hard part:
result = result.edges("|Z").edges(
BoxSelector((-WIDTH, -EPS, -EPS), (WIDTH, EPS, V_HEIGHT + EPS))
+ BoxSelector((-WIDTH, H_DEPTH - EPS, -EPS), (WIDTH, H_DEPTH + EPS, V_HEIGHT + EPS))
).fillet(R_OUTER)
OpenSCAD compiled correct geometry on the first attempt and finished in two versions. CadQuery spent about a third of its run on a single error: .fillet(3.0) failed with BRep_API: command not done, a message that names neither the edge nor the radius. The agent had to bisect by hand to discover the cause, which turned out to be arithmetic: two R3 fillets do not fit in a 4 mm wall.
T2: two parts that must fit

Both agents derived the same 54.8 x 30.8 x 25.0 mm box from the clearance stack-up.
This is where CadQuery’s parameter chain earned its keep. Changing the lip depth moved rim, plate, slots, groove and barb together, because each dimension is derived rather than typed twice. That kind of dimension chain is the same thing a good parametric UI exposes to the user, which we wrote about in Building a better OpenSCAD customizer. CadQuery has no Customizer equivalent, so the constants block is the entire parameter interface.
More interesting is how each side checked the fit. OpenSCAD prints numbers for someone to read:
echo(str("cavity LxW = ", cav_l, " x ", cav_w, " (clearance/side ", pcb_clr, ")"));
CadQuery asserts, and the build stops when the assertion is false:
assert BOX.val().intersect(LID.val()).Volume() < 1e-6, "box and lid interfere"
An echo only helps if somebody reads it. An assert fails the build on its own. For unattended generation that gap is most of the story.
OpenSCAD needed eight versions to CadQuery’s five, and three of those eight went to boolean hygiene rather than design: cleaning up slivers thrown off by tangent and coincident faces.
T3: the real thread
The hard task produced the biggest upset. OpenSCAD finished in one version, correct on the first compile, in 43 ms, with no library.
OpenSCAD has no sweep operation, so the agent wrote the helix as raw vertex and face arithmetic: one four-point ISO profile, 96 sections per turn, emitted as a single polyhedron. There are no guardrails in this code at all. Get the winding order wrong and the tool says nothing.
module helical_thread(turns, z0) {
prof = [ [r_in, -flank_hz], [r_maj, -crest_hz],
[r_maj, crest_hz], [r_in, flank_hz] ];
n = round(turns * STEPS);
pts = [ for (i = [0:n]) let(a = i * 360 / STEPS,
zo = z0 + i * thread_pitch / STEPS)
for (j = [0:3])
[ prof[j][0] * cos(a), prof[j][0] * sin(a), prof[j][1] + zo ] ];
fcs = concat(
[ [3, 2, 1, 0] ],
[ for (i = [0:n-1]) for (j = [0:3]) let(k = (j + 1) % 4)
[ 4*i + j, 4*i + k, 4*(i+1) + k, 4*(i+1) + j ] ],
[ [4*n + 0, 4*n + 1, 4*n + 2, 4*n + 3] ]
);
polyhedron(points = pts, faces = fcs, convexity = 8);
}
The agent also pre-empted the classic thread trap before writing a line: an ISO tooth spans exactly one pitch, so consecutive root flats land coplanar and the union goes bad. It sank the swept profile below the minor radius so the helix crosses the core instead of touching it. That is why the boolean was a non-event.
CadQuery expresses the same geometry in six readable lines, and that part worked first try:
helix = cq.Wire.makeHelix(pitch=THREAD_P, height=h, radius=R_ROOT, center=(0, 0, z0))
prof = (cq.Workplane("XZ", origin=(0, 0, z0)).center(R_ROOT, 0)
.polyline(thread_profile_points()).close())
ridge = prof.sweep(path, isFrenet=True)
core = cq.Workplane("XY", origin=(0, 0, z0)).circle(R_ROOT).extrude(h)
rod = core.union(ridge)
The failure came on the last line, and it is the one result that changed how we think about the QA loop.

CadQuery T3 version 1 on the left. The threaded section is nothing but floating helical turns.
union() silently discarded the core cylinder because the thread root sat exactly on the core radius. Exact tangency along a helical curve, and OCCT dropped a solid without a word. From the outside the part looked perfect. The agent read four renders without noticing. What caught it was a volume measurement: 7065 mm³ where 10323 was expected.
Worse, the broken part reported valid=True and solids=1. Raising the boolean tolerance to 1e-3 produced a solid of negative volume that also reported valid=True.
OpenSCAD is not innocent here either. In T2 its Manifold backend certified Status: NoError for an STL carrying 4 non-manifold edges and 60 zero-area triangles. Neither tool’s self-report is the last word, which is why we parsed every mesh ourselves.

Both finished threads. Crests on the left flank sit half a pitch off those on the right, which is the signature of a true single-start helix and the check that tells a real thread from stacked rings.
Download the parts
Here are the eight final meshes, exactly as the agents exported them, with no cleanup or repair from us. Binary STL, millimetres, oriented for printing with the part sitting on z = 0.
Every one of the eight is watertight, a single connected component, with zero non-manifold edges, zero boundary edges and zero flipped faces. Clearances assume FDM with a 0.4 nozzle, so each T2 pair should snap together as printed.
The triangle counts are worth a glance, and they do not favour one tool consistently. CadQuery’s box carries almost five times the triangles of the OpenSCAD box, while its lid has fewer. Mesh density here follows how much rounded detail each agent chose to add, not the kernel.
What we found
Renders caught nothing that mattered
This is the result we did not expect. Across six runs, images caught coarse blunders, like four mounting posts deleted by a cavity subtraction, and nothing subtle. Every defect that would have ruined a print was found by a number instead: a volume, an angle, an interference test, a strain calculation. In T3 the OpenSCAD agent had the opposite problem and nearly rejected correct geometry, because a thread close-up rendered in a way that looked wrong. Its own note was that the render was not sufficient evidence in either direction.
The failure modes are mirror images
CadQuery fails loudly and early. Its messages are poor, but an exception stops the run, and a stopped model cannot ship by accident. OpenSCAD fails silently and late: in T2 it reported no errors and no warnings across roughly 45 invocations while producing deleted posts, misplaced slots, and a corrupted export it had just certified as clean. For unattended generation, silent success is the more expensive failure.
CadQuery can be interrogated, OpenSCAD cannot
CadQuery answers questions about its own geometry. The T1 agent proved its countersink was 90 degrees and 9 across by reading the cone’s half-angle off the B-rep, then wired the spec into asserts that re-ran on every build. OpenSCAD has no way to query geometry, so both OpenSCAD agents independently wrote binary STL parsers, roughly as much code as the models themselves, to measure what they had built. It works, but it only sees the mesh after export, never the design.
Speed favours OpenSCAD, and it barely matters
Geometry recompute is 30 to 100 times faster, 16 ms against 1.9 s. Inside an agent loop dominated by model inference that is noise. It would matter for a live customizer or a large parameter sweep.
OpenSCAD renders have no concept of a part edge
After CSG there is only a triangle soup, so --view=edges draws the triangulation rather than an outline. CadQuery’s B-rep knows where two faces actually meet. Since images are the agent’s feedback channel, this was a real asymmetry in our setup, so we fixed it by rendering the exported STL and reconstructing outlines from the angle between adjacent faces.

Same part, three renderers. Left: the CLI default, no outlines. Middle:--view=edges, which draws every facet. Right: feature-edge reconstruction from the STL, which also paints mesh defects in red. All six runs predate this fix.
Takeaways
Neither tool wins on output. Six clean printable parts, three from each.
What decides it is verifiability rather than expressiveness, and CadQuery leads there by a wider margin than the syntax difference suggests. Being able to ask the kernel what you just built, and to assert on the answer, is worth more in an agent loop than any amount of convenient syntax.
Task shape decides more than the tool does. Simple prismatic work went to OpenSCAD. Two parts that must mate went to CadQuery. The helical thread, the task that looked tailor-made for a B-rep kernel, went to OpenSCAD outright.
If a generation pipeline leans on screenshots, it leans on the one channel that caught nothing here. Numeric assertions are the thing to force: wall thickness, clearance, interference volume, overhang angle.
Verify the mesh independently, because both toolchains certified geometry that was wrong. This is the same lesson the Pantheon benchmark produced from a different angle, where Codex looked strong in the preview loop but shipped an STL with geometry problems around the portico roof. Preview and export are not the same thing. For anything going to print, the exported mesh needs its own inspection pass.
Caveats
One agent per cell, so some of the spread is agent variance rather than tool difference.
BOSL2 was deliberately not used. A library-assisted OpenSCAD run would likely change T2 and T3 substantially.
All six runs predate the outline renderer, so the visual channel was uneven while they ran.
This measured construction only. Nobody measured the harder skill, which is repairing a model somebody else broke.
What this means for ModelRift
We are staying on OpenSCAD, and this benchmark sharpened why rather than changing the answer. The original reasons still hold: a compact text format an LLM can write directly, safe sandboxing, and fast rendering, all covered in Why we built ModelRift on OpenSCAD. CadQuery’s advantage is real but sits mostly in verification, and verification is something we can add around OpenSCAD. Its disadvantages are structural for our case: a Python runtime per session instead of a WASM sandbox, two-second rebuilds instead of 16 ms, and no clean multi-object color export of the kind we built for multicolor 3MF.
The more useful finding is about feedback. The Pantheon benchmark ended on the observation that autonomous generation is not the right workflow yet, and that Annotation Mode, where you draw on a render and hand it back to the AI, is what makes spatial iteration work. This run adds a boundary to that. Visual feedback is the right tool for massing, proportion, and “the column is in the wrong place”. It is the wrong tool for wall thickness, clearance, and whether a boolean quietly deleted half your part. Those need numbers, and the agent has to be made to look at them.
Two changes are already going back into the published OpenSCAD skill. The feature-edge renderer above, so agents get previews with real part outlines instead of triangulation. And a numeric check pass the agent has to run before it can call a model finished: wall thickness, clearances, interference, and a mesh audit that does not ask the tool whether it did a good job.