ModelRift이 Claude Opus 5 에이전트를 이용해 코드 기반 CAD 도구인 OpenSCAD와 CadQuery를 통제된 조건에서 비교한 벤치마크 결과다. 여섯 에이전트가 세 가지 과제(선반 브래킷, 스냅핏 케이스, 나사산 호스 어댑터)를 수행한 결과 모두 출력 가능한 부품을 만들었으며, 순수 성능 차이보다는 '실패 방식'에서 두 도구가 뚜렷이 갈렸다. CadQuery는 기하학 재계산에 최대 2초가 걸리는 등 느렸고, OpenSCAD는 잘못된 기하학을 조용히 생성하는 경우가 더 많았다.
번역된 본문
ModelRift은 플랫폼의 모든 모델에 OpenSCAD를 생성합니다. 이 선택이 여전히 옳은지 가끔 재검증할 가치가 있어서, 코드 기반 CAD의 가장 유력한 대안인 CadQuery(OpenCascade B-rep 커널 기반의 Python 라이브러리)와 통제된 비교를 진행했습니다. 질문은 좁혔습니다: 아무도 지켜보지 않을 때 AI 에이전트가 어느 쪽을 이용해 정확하고 출력 가능하며 기능하는 부품을 만들어낼 수 있는가? 손으로 직접 쓰기에 어느 쪽이 쾌적한지는 고려 대상이 아니었습니다. 여섯 에이전트, 세 가지 과제, 두 가지 도구. 결과물 STL은 모두 어느 도구도 신뢰하지 않는 파서로 검증했습니다. 여섯 부품 모두 출력 가능하게 나왔습니다. 성능 자체는 지루할 정도로 예상대로였습니다. 두 도구가 갈린 지점은 실패하는 방식이었습니다. 세트 중 가장 어려운 과제는 양쪽 도구 모두 해결했습니다. 왼쪽이 OpenSCAD, 오른쪽이 CadQuery입니다.
설정
여섯 번의 실행은 모두 Claude Opus 5(1M 컨텍스트)가 Claude Code를 통해 구동했습니다. 각 셀은 동일한 모델을 상속하는 별도의 범용 서브에이전트로 실행되었으며 상호 통신은 없었습니다. 셀당 에이전트 하나였으므로 아래 결과의 편차 일부는 도구 차이가 아니라 에이전트 분산입니다. 도구 버전은 CadQuery 2.8.0(Python 3.14)과 OpenSCAD 2026.06.12, 모두 M 시리즈 Mac에서 실행했습니다. OpenSCAD 쪽은 우리가 공개하고 내부에서 사용하는 에이전트 스킬인 openscad-skill로 실행했습니다. 이 스킬은 파일 및 버전 네이밍, 렌더-검사-수정 QA 루프, CLI 프리뷰용 카메라 프리셋, 단면 디버깅, Customizer 문법을 다룹니다. CadQuery용으로는 이 스킬을 작업 단위로 이식하되 구조는 같게 유지하고 OpenSCAD에 대응물이 없는 부분만 교체했습니다. CadQuery에는 CLI 렌더러가 없어서 소형 오프스크린 렌더러를 새로 작성해야 했고, CSG 상태 라인 대신 B-rep 유효성 지표를 넣었습니다. 두 스킬 파일 모두 동일한 3D 프린팅 설계 규칙(벽 두께, 여유 공차, 오버행)을 담아 어느 쪽도 상대에게 없는 조언을 받지 않도록 했습니다. 파일 크기는 OpenSCAD 12.4KB, CadQuery 13.1KB였습니다. 한 가지 비대칭은 남았습니다. CadQuery 파일에는 OpenSCAD 파일에 필요 없는 API 치트시트가 포함되어 있는데, 모델이 이미 OpenSCAD 문법을 잘 알기 때문입니다. 이는 문법 면에서 CadQuery에 도움이 되지만 기하학에는 아무 도움이 안 됩니다. 각 에이전트는 최대 12버전까지 제한되었고, 성공을 위조하지 말 것, 모든 실패를 오류 원문 그대로 보고할 것을 요구받았습니다. 무인으로 실행되었습니다.
우리는 에이전트의 말을 그대로 믿지 않았습니다. 최종 STL마다 파일을 직접 읽어 삼각형 수, 바운딩 박스, 부피, 워터타이트 여부, 비매니폴드 및 경계 에지, 뒤집힌 면, 연결 성분 수를 보고하는 파서를 통과시켰습니다. 이것이 예상보다 중요한 것으로 판명났는데, 이유는 아래에 나옵니다.
세 가지 과제
같은 과제의 두 에이전트는 동일한 텍스트를 받았고 도구별 힌트는 없었습니다. 간단한 T1은 벽걸이 선반용 L자 브래킷입니다. 90도로 만나는 두 판(두께 4), 삼각형 거셋 2개, 평头 나사용 카운터싱크 홀 2개(90도, 머리 지름 9), 일반 홀 2개, 외측 수직 모서리 R3, 안쪽 모서리 R4 필렛, 서포트 없이 출력 가능해야 합니다. T2는 난이도를 올려 50 x 26 PCB용 두 부품 스냅핏 케이스입니다. 벽 두께 2, 캐비티 공차 면당 0.4, M2 포스트 4개, 9.5 x 3.5 USB-C 컷아웃, 주변 립(공차 0.2)과 작동하는 스냅이 달린 별도 뚜껑, 통풍 슬롯 5개. 두 부품이 실제로 맞아야 했습니다. T3은 가장 어려웠습니다. M24x2 나사산 호스 바브 어댑터로, 대변 거리 30의 육각 플랜지, 길이 12의 실제 나선 나사산, 내경 12 호스용 바브 3개가 달린 길이 25의 바브, 지름 8 관통 채널입니다. 스펙은 링 쌓기를 명시적으로 금지했고 나사산은 진짜 나선 기하학이어야 했습니다.
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 );