Text to CAD (CadQuery)
This skill converts a natural language description of a 3D object into a fully functional CadQuery Python script, executes it, and delivers STL + STEP files. The workflow is designed to handle everything from simple primitives ("a cube with rounded edges") to complex mechanical assemblies ("a flanged bearing housing with bolt holes").
Phase 0: Environment Detection & Setup
Before generating any model, automatically detect a working CadQuery environment. Follow this sequence -- stop at the first success:
Check if cadquery is already importable:
python -c "import cadquery; print(cadquery.__version__)"
If this succeeds, use python directly as the interpreter.
Search for conda/mamba environments that have cadquery:
conda env list
For each environment found, test:
conda run -n <env_name> python -c "import cadquery; print(cadquery.__version__)"
If one succeeds, use conda run -n <env_name> python as the interpreter.
Search for virtual environments in the working directory or common locations (.venv, venv, env):
# Linux/macOS
.venv/bin/python -c "import cadquery; print(cadquery.__version__)"
# Windows
.venv/Scripts/python -c "import cadquery; print(cadquery.__version__)"
If no environment found, install cadquery:
- Preferred:
pip install cadquery (in current Python)
- Fallback:
conda install -c conda-forge cadquery (if conda is available)
- Confirm installation succeeded before proceeding.
Cache the result: Once a working interpreter command is found, reuse it for all subsequent executions in this session. Store it as CADQUERY_PYTHON (e.g., python, conda run -n myenv python, .venv/bin/python).
If all attempts fail, inform the user and provide manual installation instructions:
pip install cadquery
# or
conda install -c conda-forge cadquery
Phase 1: Requirement Analysis & Clarification
When the user provides a natural language description:
Parse the description to extract:
- Geometry type: primitive (box, cylinder, sphere), composite, or assembly
- Dimensions: explicit measurements (mm by default) or relative sizing
- Features: holes, fillets, chamfers, patterns, text, threads, etc.
- Spatial relationships: positions, alignments, symmetry
- Material/functional hints: load-bearing, aesthetic, printable, etc.
Fill in missing details intelligently:
- If no units specified -> assume millimeters (mm)
- If no dimensions specified -> infer reasonable engineering defaults based on the object type
- If ambiguous geometry -> choose the most common/standard engineering interpretation
- If "printable" mentioned -> ensure manifold geometry, add appropriate tolerances
Confirm understanding (brief, 2-3 sentences):
- Summarize what you will model
- State key dimensions and features
- Note any assumptions made
- Ask the user to confirm or adjust before proceeding
Phase 2: Code Generation
Generate a complete, self-contained CadQuery Python script following these mandatory rules:
Code Structure Template
"""
CadQuery Model: {model_name}
Description: {user_description}
Generated dimensions: {key_dimensions}
Units: millimeters (mm)
"""
import cadquery as cq
import os
# ============================================================
# Parameters (easy to modify)
# ============================================================
# Group all dimensional parameters at the top for easy tweaking
PARAM_NAME = value # description, unit
# ============================================================
# Output Configuration
# ============================================================
# Output to an "output" folder relative to this script's location.
# The user can override OUTPUT_DIR if they prefer a different path.
OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
MODEL_NAME = "{model_name}"
os.makedirs(OUTPUT_DIR, exist_ok=True)
# ============================================================
# Model Construction
# ============================================================
# Build the model step by step with comments explaining each operation
result = (
cq.Workplane("XY")
.box(...)
# ... operations ...
)
# ============================================================
# Export
# ============================================================
step_path = os.path.join(OUTPUT_DIR, f"{MODEL_NAME}.step")
stl_path = os.path.join(OUTPUT_DIR, f"{MODEL_NAME}.stl")
cq.exporters.export(result, step_path)
cq.exporters.export(result, stl_path)
print(f"Model '{MODEL_NAME}' generated successfully!")
print(f" STEP: {step_path}")
print(f" STL: {stl_path}")
# Print bounding box for verification
bb = result.val().BoundingBox()
print(f" Bounding Box: {bb.xlen:.2f} x {bb.ylen:.2f} x {bb.zlen:.2f} mm")
CadQuery API Best Practices
Primitives & Basic Shapes:
cq.Workplane("XY").box(length, width, height) -- centered box
cq.Workplane("XY").cylinder(height, radius) -- centered cylinder
cq.Workplane("XY").sphere(radius) -- sphere
cq.Workplane("XY").wedge(dx, dy, dz, xmin, zmin, xmax, zmax) -- wedge/prism
2D Sketch -> 3D Extrusion (most versatile pattern):
result = (
cq.Workplane("XY")
.moveTo(x, y).lineTo(...).lineTo(...).close() # sketch profile
.extrude(height) # or .revolve(angleDegrees, axisStart, axisEnd)
)
Feature Operations:
.fillet(radius) -- round all edges (use with .edges("|Z") etc. for selective)
.chamfer(distance) -- chamfer edges
.hole(diameter, depth=None) -- through or blind hole at center
.cboreHole(diameter, cboreDiameter, cboreDepth) -- counterbore hole
.cskHole(diameter, cskDiameter, cskAngle) -- countersink hole
.shell(thickness) -- hollow out (negative = inward)
Face/Edge Selection (critical for targeted operations):
.faces(">Z") -- topmost face in Z
.faces("<Z") -- bottommost face in Z
.edges("|Z") -- edges parallel to Z
.edges(">Z") -- topmost edges in Z
.edges("%Circle") -- circular edges
.faces("+Z") -- faces with normal pointing in +Z direction
Boolean Operations:
.cut(other_shape) -- subtract
.union(other_shape) -- add
.intersect(other_shape) -- intersection
Patterns & Arrays:
.pushPoints([(x1,y1), (x2,y2), ...]) -- place features at points
.rarray(xSpacing, ySpacing, xCount, yCount) -- rectangular array
.polarArray(radius, startAngle, angle, count) -- circular array
Advanced:
.sweep(path) -- sweep a profile along a path
.loft() -- loft between profiles
.twistExtrude(height, angleDegrees) -- helical extrusion
.text("text", fontsize, distance) -- embossed/engraved text
.mirror("XY") -- mirror about a plane
.translate((x, y, z)) -- move
.rotate((0,0,0), (0,0,1), angleDeg) -- rotate
Multi-body / Assembly Pattern:
part_a = cq.Workplane("XY").box(10, 10, 10)
part_b = cq.Workplane("XY").transformed(offset=(20, 0, 0)).cylinder(10, 5)
result = part_a.union(part_b)
Code Quality Rules
- All parameters at the top -- no magic numbers in the modeling section
- Descriptive variable names --
flange_diameter, not d1
- Step-by-step comments -- explain what each operation does in context
- Build incrementally -- complex models should be built in logical stages
- Selective fillet/chamfer -- use face/edge selectors, not blanket
.fillet() which often fails
- Error-safe ordering: fillet/chamfer operations MUST come AFTER all boolean cuts/unions. Fillets on edges that get modified by later booleans will crash
- Manifold geometry -- ensure the result is a valid solid (no self-intersections)
- Reasonable tolerances -- if parts need to fit together, add 0.1-0.2mm clearance
Common Pitfalls to AVOID
.fillet() with radius >= smallest edge length -> crash. Always use conservative radii.
.shell() on complex geometry with thin walls -> often fails. Keep wall thickness reasonable.
- Chaining too many operations without
.clean() -> geometry corruption. Add .clean() after complex booleans.
- Forgetting that
.box() and .cylinder() are centered by default.
- Using
.faces(">Z").fillet() when there are multiple faces at the same Z height -> ambiguous selection.
- Applying
.fillet() before .cut() -- fillet edges may be destroyed by the cut.
Phase 3: Execution
- Determine the working directory: Use the user's current working directory (or a temporary directory) to write the script. Write the script to
{working_dir}/{model_name}.py.
- Execute using the interpreter found in Phase 0:
{CADQUERY_PYTHON} {working_dir}/{model_name}.py
Where {CADQUERY_PYTHON} is the cached interpreter command from environment detection.
- Set timeout to 60 seconds (complex models may take time)
Phase 4: Auto-Debug (up to 5 attempts)
If execution fails, follow this diagnostic protocol:
| Error Type |
Diagnosis |
Fix Strategy |
Standard_ConstructionError |
Fillet/chamfer radius too large |
Reduce radius to 50% of smallest adjacent edge |
BRep_API: not done |
Boolean operation failed |
Add .clean() before boolean; simplify geometry |
StdFail_NotDone |
Impossible geometric operation |
Re-order operations; split into sub-steps |
ValueError: No wire found |
Unclosed sketch profile |
Ensure .close() is called; check .lineTo() endpoints |
Selector found no objects |
Face/edge selector matched nothing |
Use simpler selectors; print available faces/edges for debugging |
ModuleNotFoundError |
Missing package |
Install via pip install {package} using the detected interpreter's environment, then retry. If cadquery itself is missing, re-run Phase 0 |
MemoryError or timeout |
Model too complex |
Reduce polygon count; simplify fillets |
Debug approach:
- Read the full traceback
- Identify the exact failing CadQuery operation
- Apply the targeted fix from the table above
- If unclear, add diagnostic prints:
print(result.faces().vals()) to inspect geometry state
- Rebuild and re-execute
Phase 5: Result Verification & Delivery
After successful execution:
Verify output files exist and have non-zero size
Report to user:
- Confirmation of success
- Bounding box dimensions (X x Y x Z mm)
- File paths (STEP and STL)
- Brief description of modeling approach
- Suggestions for modifications (optional parameters to tweak)
Offer follow-up options:
- "Want me to adjust any dimensions?"
- "Need additional features (holes, fillets, text)?"
- "Want to generate a variant or assembly?"
- "Need the code explained step by step?"
Phase 6: Iterative Refinement
If the user requests changes:
- Read the existing script to understand current state
- Apply targeted modifications -- don't regenerate from scratch unless major restructuring is needed
- Re-execute and verify with the same pipeline
- Show diff -- briefly describe what changed
Mechanical Parts Library (reference patterns)
Bolt/Screw:
head = cq.Workplane("XY").cylinder(head_height, head_radius)
shaft = cq.Workplane("XY").workplane(offset=-head_height).cylinder(shaft_length, shaft_radius)
result = head.union(shaft)
Gear (simplified profile):
result = (
cq.Workplane("XY")
.circle(outer_radius)
.extrude(thickness)
.faces(">Z")
.workplane()
.hole(bore_diameter)
.faces(">Z")
.workplane()
.polarArray(pitch_radius, 0, 360, num_teeth)
.rect(tooth_width, tooth_height)
.cutThruAll()
)
Enclosure/Box with lid:
body = cq.Workplane("XY").box(L, W, H).edges("|Z").fillet(corner_r).shell(-wall)
lid = cq.Workplane("XY").workplane(offset=H/2).box(L, W, lid_h).edges("|Z").fillet(corner_r)
Pipe/Tube:
result = (
cq.Workplane("XY")
.circle(outer_radius)
.circle(inner_radius) # concentric circle creates annular profile
.extrude(length)
)
Flange:
result = (
cq.Workplane("XY")
.circle(flange_radius).extrude(flange_thickness)
.faces(">Z").workplane()
.circle(pipe_radius).extrude(pipe_length)
.faces("<Z").workplane()
.pushPoints(bolt_hole_positions)
.hole(bolt_hole_diameter)
.faces("<Z").workplane()
.hole(bore_diameter)
)
Quality Checklist (verify before delivering)
Response Language
Always respond in the same language as the user's message. If the user writes in Chinese, respond in Chinese. If in English, respond in English.
1---2name: text-to-cad3description: Use when the user provides a natural language description of a 3D object or mechanical part and wants to generate a CAD model. Converts the description into CadQuery Python code, automatically detects or sets up the CadQuery environment, executes the script, and produces STL and STEP output files.4---5
6# Text to CAD (CadQuery)
7
8This skill converts a natural language description of a 3D object into a fully functional CadQuery Python script, executes it, and delivers STL + STEP files. The workflow is designed to handle everything from simple primitives ("a cube with rounded edges") to complex mechanical assemblies ("a flanged bearing housing with bolt holes").
9
10## Phase 0: Environment Detection & Setup
11
12Before generating any model, **automatically detect a working CadQuery environment**. Follow this sequence -- stop at the first success:
13
141. **Check if `cadquery` is already importable**:
15 ```bash
16 python -c "import cadquery; print(cadquery.__version__)"
17 ```
18 If this succeeds, use `python` directly as the interpreter.
19
202. **Search for conda/mamba environments that have cadquery**:
21 ```bash
22 conda env list
23 ```
24 For each environment found, test:
25 ```bash
26 conda run -n <env_name> python -c "import cadquery; print(cadquery.__version__)"
27 ```
28 If one succeeds, use `conda run -n <env_name> python` as the interpreter.
29
303. **Search for virtual environments in the working directory or common locations** (`.venv`, `venv`, `env`):
31 ```bash
32 # Linux/macOS
33 .venv/bin/python -c "import cadquery; print(cadquery.__version__)"
34 # Windows
35 .venv/Scripts/python -c "import cadquery; print(cadquery.__version__)"
36 ```
37
384. **If no environment found, install cadquery**:
39 - Preferred: `pip install cadquery` (in current Python)
40 - Fallback: `conda install -c conda-forge cadquery` (if conda is available)
41 - Confirm installation succeeded before proceeding.
42
435. **Cache the result**: Once a working interpreter command is found, reuse it for all subsequent executions in this session. Store it as `CADQUERY_PYTHON` (e.g., `python`, `conda run -n myenv python`, `.venv/bin/python`).
44
45If all attempts fail, inform the user and provide manual installation instructions:
46```
47pip install cadquery
48# or
49conda install -c conda-forge cadquery
50```
51
52---
53
54## Phase 1: Requirement Analysis & Clarification
55
56When the user provides a natural language description:
57
581. **Parse the description** to extract:
59 - **Geometry type**: primitive (box, cylinder, sphere), composite, or assembly
60 - **Dimensions**: explicit measurements (mm by default) or relative sizing
61 - **Features**: holes, fillets, chamfers, patterns, text, threads, etc.
62 - **Spatial relationships**: positions, alignments, symmetry
63 - **Material/functional hints**: load-bearing, aesthetic, printable, etc.
64
652. **Fill in missing details intelligently**:
66 - If no units specified -> assume millimeters (mm)
67 - If no dimensions specified -> infer reasonable engineering defaults based on the object type
68 - If ambiguous geometry -> choose the most common/standard engineering interpretation
69 - If "printable" mentioned -> ensure manifold geometry, add appropriate tolerances
70
713. **Confirm understanding** (brief, 2-3 sentences):
72 - Summarize what you will model
73 - State key dimensions and features
74 - Note any assumptions made
75 - Ask the user to confirm or adjust before proceeding
76
77---
78
79## Phase 2: Code Generation
80
81Generate a complete, self-contained CadQuery Python script following these **mandatory rules**:
82
83### Code Structure Template
84
85```python
86"""
87CadQuery Model: {model_name}
88Description: {user_description}
89Generated dimensions: {key_dimensions}
90Units: millimeters (mm)
91"""
92
93import cadquery as cq
94import os
95
96# ============================================================
97# Parameters (easy to modify)
98# ============================================================
99# Group all dimensional parameters at the top for easy tweaking
100PARAM_NAME = value # description, unit
101
102# ============================================================
103# Output Configuration
104# ============================================================
105# Output to an "output" folder relative to this script's location.
106# The user can override OUTPUT_DIR if they prefer a different path.
107OUTPUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output")
108MODEL_NAME = "{model_name}"
109
110os.makedirs(OUTPUT_DIR, exist_ok=True)
111
112# ============================================================
113# Model Construction
114# ============================================================
115# Build the model step by step with comments explaining each operation
116
117result = (
118 cq.Workplane("XY")
119 .box(...)
120 # ... operations ...
121)
122
123# ============================================================
124# Export
125# ============================================================
126step_path = os.path.join(OUTPUT_DIR, f"{MODEL_NAME}.step")
127stl_path = os.path.join(OUTPUT_DIR, f"{MODEL_NAME}.stl")
128
129cq.exporters.export(result, step_path)
130cq.exporters.export(result, stl_path)
131
132print(f"Model '{MODEL_NAME}' generated successfully!")
133print(f" STEP: {step_path}")
134print(f" STL: {stl_path}")
135
136# Print bounding box for verification
137bb = result.val().BoundingBox()
138print(f" Bounding Box: {bb.xlen:.2f} x {bb.ylen:.2f} x {bb.zlen:.2f} mm")
139```
140
141### CadQuery API Best Practices
142
143**Primitives & Basic Shapes:**
144- `cq.Workplane("XY").box(length, width, height)` -- centered box
145- `cq.Workplane("XY").cylinder(height, radius)` -- centered cylinder
146- `cq.Workplane("XY").sphere(radius)` -- sphere
147- `cq.Workplane("XY").wedge(dx, dy, dz, xmin, zmin, xmax, zmax)` -- wedge/prism
148
149**2D Sketch -> 3D Extrusion (most versatile pattern):**
150```python
151result = (
152 cq.Workplane("XY")
153 .moveTo(x, y).lineTo(...).lineTo(...).close() # sketch profile
154 .extrude(height) # or .revolve(angleDegrees, axisStart, axisEnd)
155)
156```
157
158**Feature Operations:**
159- `.fillet(radius)` -- round all edges (use with `.edges("|Z")` etc. for selective)
160- `.chamfer(distance)` -- chamfer edges
161- `.hole(diameter, depth=None)` -- through or blind hole at center
162- `.cboreHole(diameter, cboreDiameter, cboreDepth)` -- counterbore hole
163- `.cskHole(diameter, cskDiameter, cskAngle)` -- countersink hole
164- `.shell(thickness)` -- hollow out (negative = inward)
165
166**Face/Edge Selection (critical for targeted operations):**
167- `.faces(">Z")` -- topmost face in Z
168- `.faces("<Z")` -- bottommost face in Z
169- `.edges("|Z")` -- edges parallel to Z
170- `.edges(">Z")` -- topmost edges in Z
171- `.edges("%Circle")` -- circular edges
172- `.faces("+Z")` -- faces with normal pointing in +Z direction
173
174**Boolean Operations:**
175- `.cut(other_shape)` -- subtract
176- `.union(other_shape)` -- add
177- `.intersect(other_shape)` -- intersection
178
179**Patterns & Arrays:**
180- `.pushPoints([(x1,y1), (x2,y2), ...])` -- place features at points
181- `.rarray(xSpacing, ySpacing, xCount, yCount)` -- rectangular array
182- `.polarArray(radius, startAngle, angle, count)` -- circular array
183
184**Advanced:**
185- `.sweep(path)` -- sweep a profile along a path
186- `.loft()` -- loft between profiles
187- `.twistExtrude(height, angleDegrees)` -- helical extrusion
188- `.text("text", fontsize, distance)` -- embossed/engraved text
189- `.mirror("XY")` -- mirror about a plane
190- `.translate((x, y, z))` -- move
191- `.rotate((0,0,0), (0,0,1), angleDeg)` -- rotate
192
193**Multi-body / Assembly Pattern:**
194```python
195part_a = cq.Workplane("XY").box(10, 10, 10)
196part_b = cq.Workplane("XY").transformed(offset=(20, 0, 0)).cylinder(10, 5)
197result = part_a.union(part_b)
198```
199
200### Code Quality Rules
201
2021. **All parameters at the top** -- no magic numbers in the modeling section
2032. **Descriptive variable names** -- `flange_diameter`, not `d1`
2043. **Step-by-step comments** -- explain what each operation does in context
2054. **Build incrementally** -- complex models should be built in logical stages
2065. **Selective fillet/chamfer** -- use face/edge selectors, not blanket `.fillet()` which often fails
2076. **Error-safe ordering**: fillet/chamfer operations MUST come AFTER all boolean cuts/unions. Fillets on edges that get modified by later booleans will crash
2087. **Manifold geometry** -- ensure the result is a valid solid (no self-intersections)
2098. **Reasonable tolerances** -- if parts need to fit together, add 0.1-0.2mm clearance
210
211### Common Pitfalls to AVOID
212
213- `.fillet()` with radius >= smallest edge length -> crash. Always use conservative radii.
214- `.shell()` on complex geometry with thin walls -> often fails. Keep wall thickness reasonable.
215- Chaining too many operations without `.clean()` -> geometry corruption. Add `.clean()` after complex booleans.
216- Forgetting that `.box()` and `.cylinder()` are centered by default.
217- Using `.faces(">Z").fillet()` when there are multiple faces at the same Z height -> ambiguous selection.
218- Applying `.fillet()` before `.cut()` -- fillet edges may be destroyed by the cut.
219
220---
221
222## Phase 3: Execution
223
2241. **Determine the working directory**: Use the user's current working directory (or a temporary directory) to write the script. Write the script to `{working_dir}/{model_name}.py`.
2252. **Execute** using the interpreter found in Phase 0:
226 ```bash
227 {CADQUERY_PYTHON} {working_dir}/{model_name}.py
228 ```
229 Where `{CADQUERY_PYTHON}` is the cached interpreter command from environment detection.
2303. **Set timeout** to 60 seconds (complex models may take time)
231
232---
233
234## Phase 4: Auto-Debug (up to 5 attempts)
235
236If execution fails, follow this diagnostic protocol:
237
238| Error Type | Diagnosis | Fix Strategy |
239|---|---|---|
240| `Standard_ConstructionError` | Fillet/chamfer radius too large | Reduce radius to 50% of smallest adjacent edge |
241| `BRep_API: not done` | Boolean operation failed | Add `.clean()` before boolean; simplify geometry |
242| `StdFail_NotDone` | Impossible geometric operation | Re-order operations; split into sub-steps |
243| `ValueError: No wire found` | Unclosed sketch profile | Ensure `.close()` is called; check `.lineTo()` endpoints |
244| `Selector found no objects` | Face/edge selector matched nothing | Use simpler selectors; print available faces/edges for debugging |
245| `ModuleNotFoundError` | Missing package | Install via `pip install {package}` using the detected interpreter's environment, then retry. If cadquery itself is missing, re-run Phase 0 |
246| `MemoryError` or timeout | Model too complex | Reduce polygon count; simplify fillets |
247
248**Debug approach:**
2491. Read the full traceback
2502. Identify the exact failing CadQuery operation
2513. Apply the targeted fix from the table above
2524. If unclear, add diagnostic prints: `print(result.faces().vals())` to inspect geometry state
2535. Rebuild and re-execute
254
255---
256
257## Phase 5: Result Verification & Delivery
258
259After successful execution:
260
2611. **Verify output files exist** and have non-zero size
2622. **Report to user**:
263 - Confirmation of success
264 - Bounding box dimensions (X x Y x Z mm)
265 - File paths (STEP and STL)
266 - Brief description of modeling approach
267 - Suggestions for modifications (optional parameters to tweak)
268
2693. **Offer follow-up options**:
270 - "Want me to adjust any dimensions?"
271 - "Need additional features (holes, fillets, text)?"
272 - "Want to generate a variant or assembly?"
273 - "Need the code explained step by step?"
274
275---
276
277## Phase 6: Iterative Refinement
278
279If the user requests changes:
2801. **Read the existing script** to understand current state
2812. **Apply targeted modifications** -- don't regenerate from scratch unless major restructuring is needed
2823. **Re-execute and verify** with the same pipeline
2834. **Show diff** -- briefly describe what changed
284
285---
286
287## Mechanical Parts Library (reference patterns)
288
289**Bolt/Screw:**
290```python
291head = cq.Workplane("XY").cylinder(head_height, head_radius)
292shaft = cq.Workplane("XY").workplane(offset=-head_height).cylinder(shaft_length, shaft_radius)
293result = head.union(shaft)
294```
295
296**Gear (simplified profile):**
297```python
298result = (
299 cq.Workplane("XY")
300 .circle(outer_radius)
301 .extrude(thickness)
302 .faces(">Z")
303 .workplane()
304 .hole(bore_diameter)
305 .faces(">Z")
306 .workplane()
307 .polarArray(pitch_radius, 0, 360, num_teeth)
308 .rect(tooth_width, tooth_height)
309 .cutThruAll()
310)
311```
312
313**Enclosure/Box with lid:**
314```python
315body = cq.Workplane("XY").box(L, W, H).edges("|Z").fillet(corner_r).shell(-wall)
316lid = cq.Workplane("XY").workplane(offset=H/2).box(L, W, lid_h).edges("|Z").fillet(corner_r)
317```
318
319**Pipe/Tube:**
320```python
321result = (
322 cq.Workplane("XY")
323 .circle(outer_radius)
324 .circle(inner_radius) # concentric circle creates annular profile
325 .extrude(length)
326)
327```
328
329**Flange:**
330```python
331result = (
332 cq.Workplane("XY")
333 .circle(flange_radius).extrude(flange_thickness)
334 .faces(">Z").workplane()
335 .circle(pipe_radius).extrude(pipe_length)
336 .faces("<Z").workplane()
337 .pushPoints(bolt_hole_positions)
338 .hole(bolt_hole_diameter)
339 .faces("<Z").workplane()
340 .hole(bore_diameter)
341)
342```
343
344---
345
346## Quality Checklist (verify before delivering)
347
348- [ ] Script runs without errors
349- [ ] Both STL and STEP files generated with non-zero size
350- [ ] Bounding box matches expected dimensions (within 1%)
351- [ ] All user-specified features present
352- [ ] Parameters are clearly labeled and at the top of the script
353- [ ] Code is well-commented and readable
354- [ ] Fillets/chamfers applied AFTER all boolean operations
355- [ ] No magic numbers in modeling section
356
357---
358
359## Response Language
360
361Always respond in the **same language as the user's message**. If the user writes in Chinese, respond in Chinese. If in English, respond in English.