agentcad-cli 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. agentcad/__init__.py +3 -0
  2. agentcad/__main__.py +4 -0
  3. agentcad/_templates/__init__.py +0 -0
  4. agentcad/_templates/model/README.md +46 -0
  5. agentcad/_templates/model/design.json +43 -0
  6. agentcad/_templates/model/params.json +5 -0
  7. agentcad/_templates/model/part.py +33 -0
  8. agentcad/_templates/workspace/CLAUDE.md +420 -0
  9. agentcad/_templates/workspace/cadproject.json +7 -0
  10. agentcad/_templates/workspace/models/.gitkeep +0 -0
  11. agentcad/_templates/workspace/references/build123d-guide.md +671 -0
  12. agentcad/_templates/workspace/references/images/.gitkeep +0 -0
  13. agentcad/_templates/workspace/references/notes.md +3 -0
  14. agentcad/_templates/workspace/references/validation-strategy.md +378 -0
  15. agentcad/checks/__init__.py +130 -0
  16. agentcad/checks/mesh.py +131 -0
  17. agentcad/checks/relations.py +155 -0
  18. agentcad/checks/section.py +199 -0
  19. agentcad/cli.py +235 -0
  20. agentcad/contract.py +160 -0
  21. agentcad/geometry.py +417 -0
  22. agentcad/inspect.py +102 -0
  23. agentcad/jsonio.py +30 -0
  24. agentcad/measure.py +80 -0
  25. agentcad/payloads.py +24 -0
  26. agentcad/precheck.py +153 -0
  27. agentcad/probe.py +215 -0
  28. agentcad/render.py +166 -0
  29. agentcad/report.py +151 -0
  30. agentcad/review.py +399 -0
  31. agentcad/runner.py +182 -0
  32. agentcad/section.py +346 -0
  33. agentcad/stl.py +265 -0
  34. agentcad/templates.py +21 -0
  35. agentcad/validate.py +234 -0
  36. agentcad/workspace.py +173 -0
  37. agentcad_cli-0.1.1.dist-info/METADATA +145 -0
  38. agentcad_cli-0.1.1.dist-info/RECORD +41 -0
  39. agentcad_cli-0.1.1.dist-info/WHEEL +5 -0
  40. agentcad_cli-0.1.1.dist-info/entry_points.txt +2 -0
  41. agentcad_cli-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,671 @@
1
+ # build123d Modeling Guide
2
+
3
+ Reference for writing `part.py` files. Uses the Builder API (context managers).
4
+ `from build123d import *` imports everything needed.
5
+
6
+ ## Builder Context Managers
7
+
8
+ All geometry is created inside builders. Builders track all objects created within
9
+ them and allow query/selection of topology.
10
+
11
+ ### BuildPart — 3D solid modeling
12
+
13
+ ```python
14
+ with BuildPart() as bp:
15
+ add(Box(30, 20, 10)) # base solid
16
+ fillet(bp.edges()[-4:], radius=2) # modify edges
17
+ with Locations((0, 0, 10)): # position context
18
+ Cylinder(radius=5, height=8) # add feature
19
+ result = bp.part # extract the Shape
20
+ ```
21
+
22
+ Key methods on `bp`:
23
+ - `bp.part` — the final Compound/Shape
24
+ - `bp.solids()` — all solid bodies
25
+ - `bp.faces()` — all faces
26
+ - `bp.edges()` — all edges
27
+ - `bp.vertices()` — all vertices
28
+
29
+ ### BuildSketch — 2D profiles
30
+
31
+ ```python
32
+ with BuildSketch() as sk:
33
+ Rectangle(30, 20)
34
+ with Locations((0, 0)):
35
+ Circle(radius=5, mode=Mode.SUBTRACT)
36
+ result_sketch = sk.sketch
37
+ ```
38
+
39
+ BuildSketch operates on the default Plane.XY (Z=0) unless a workplane is
40
+ specified. Sketches are extruded via `extrude()` inside BuildPart, or used
41
+ with `add(Sketch)`.
42
+
43
+ ### BuildLine — 1D wire paths
44
+
45
+ ```python
46
+ with BuildLine() as ln:
47
+ l1 = Line((0, 0), (10, 0))
48
+ l2 = Line((10, 0), (10, 5))
49
+ l3 = Line((10, 5), (0, 0))
50
+ result_line = ln.line
51
+ ```
52
+
53
+ Used for sweep paths, lofts, and complex profiles.
54
+
55
+ ## Key Enums
56
+
57
+ ### Align — positioning on each axis
58
+
59
+ ```
60
+ Align.MIN = toward negative end (-X, -Y, or -Z)
61
+ Align.CENTER = centered (default for most objects)
62
+ Align.MAX = toward positive end (+X, +Y, or +Z)
63
+ ```
64
+
65
+ All 3D primitives accept `align=(x, y, z)` tuple. Example:
66
+ ```python
67
+ # Box sitting on Z=0 plane, centered in X/Y
68
+ Box(30, 20, 10, align=(Align.CENTER, Align.CENTER, Align.MIN))
69
+
70
+ # Cylinder growing upward from origin
71
+ Cylinder(radius=5, height=20, align=(Align.CENTER, Align.CENTER, Align.MIN))
72
+ ```
73
+
74
+ Defaults: Box = CENTER on all axes. Cylinder = CENTER X/Y, MIN Z.
75
+
76
+ ### Mode — how objects combine
77
+
78
+ ```
79
+ Mode.ADD = boolean union (default)
80
+ Mode.SUBTRACT = boolean cut
81
+ Mode.INTERSECT = boolean intersection
82
+ Mode.REPLACE = replace the builder's shape entirely
83
+ Mode.PRIVATE = create but don't combine with the builder
84
+ ```
85
+
86
+ ```python
87
+ # Subtract a circle from a rectangle (cut a hole)
88
+ with BuildSketch() as sk:
89
+ Rectangle(30, 20)
90
+ Circle(radius=5, mode=Mode.SUBTRACT)
91
+ ```
92
+
93
+ ### Select — which objects an operation targets
94
+
95
+ ```
96
+ Select.ALL = operate on all topology in builder
97
+ Select.LAST = operate on most recently created objects
98
+ Select.NEW = operate on objects not yet consumed (default for many ops)
99
+ ```
100
+
101
+ Most operations default to Select.NEW. `fillet` and `chamfer` accept an
102
+ explicit list of edges/faces.
103
+
104
+ ### GeomType — edge/face geometry classification
105
+
106
+ ```
107
+ GeomType.LINE = straight line edge
108
+ GeomType.CIRCLE = circular arc or full circle
109
+ GeomType.ELLIPSE = elliptical arc
110
+ GeomType.BSPLINE = spline curve
111
+ GeomType.PLANE = flat face
112
+ GeomType.CYLINDER = cylindrical face
113
+ GeomType.CONE = conical face
114
+ GeomType.SPHERE = spherical face
115
+ ```
116
+
117
+ Used with `filter_by(GeomType.LINE)` to narrow edge/face selections.
118
+
119
+ ### SortBy — alternative sort criteria
120
+
121
+ ```
122
+ SortBy.RADIUS = sort circles/arcs by radius
123
+ SortBy.LENGTH = sort edges by length
124
+ SortBy.AREA = sort faces by area
125
+ SortBy.DISTANCE = sort by distance from origin
126
+ ```
127
+
128
+ ## 3D Primitives
129
+
130
+ | Object | Key parameters |
131
+ |--------|---------------|
132
+ | `Box(length, width, height, align)` | Rectangular solid |
133
+ | `Cylinder(radius, height, align, rotation)` | Cylindrical solid |
134
+ | `Cone(bottom_radius, top_radius, height, align)` | Truncated cone. `top_radius=0` for full cone |
135
+ | `Sphere(radius)` | Spherical solid |
136
+ | `Torus(outer_ring_radius, tube_radius)` | Toroidal solid |
137
+ | `Wedge(dx, dy, dz, xmin, zmin, xmax, zmax)` | Wedge with optional angle cuts |
138
+
139
+ ```python
140
+ # Box aligned to sit on Z=0
141
+ add(Box(40, 30, 10, align=(Align.CENTER, Align.CENTER, Align.MIN)))
142
+
143
+ # Cone (chamfer-like taper)
144
+ add(Cone(bottom_radius=10, top_radius=8, height=5))
145
+
146
+ # Cylinder with specific rotation
147
+ add(Cylinder(radius=5, height=20, rotation=(90, 0, 0))) # rotated to lie along X
148
+ ```
149
+
150
+ ## 2D Primitives (for BuildSketch)
151
+
152
+ | Object | Key parameters |
153
+ |--------|---------------|
154
+ | `Rectangle(width, height, align)` | Aligned rectangle |
155
+ | `Circle(radius)` | Circle at origin |
156
+ | `Ellipse(major_radius, minor_radius)` | Ellipse |
157
+ | `Polygon(points)` | Polygon from point list |
158
+ | `Polygon(pts, side_count)` | Regular polygon |
159
+ | `Trapezoid(width, height, left_side_angle)` | Trapezoid |
160
+ | `Slot(width, height)` | Rounded-end slot |
161
+ | `Triangle(a, b, c)` | Triangle from side lengths |
162
+
163
+ All 2D objects support `mode=Mode.SUBTRACT` to cut from the sketch.
164
+
165
+ ## 1D Primitives (for BuildLine)
166
+
167
+ | Object | Key parameters |
168
+ |--------|---------------|
169
+ | `Line(start, end)` | Straight line between two points |
170
+ | `Arc(start, middle, end)` | Circular arc through three points |
171
+ | `RadiusArc(start, end, radius)` | Arc defined by radius |
172
+ | `TangentArc(start, end, tangent)` | Arc tangent to direction at start |
173
+ | `Spline(points)` | BSpline through points |
174
+ | `PolarLine(start, length, angle)` | Line at angle from start |
175
+ | `Helix(pitch, height, radius)` | Helical curve (for threads) |
176
+
177
+ ## Operations
178
+
179
+ ### 3D Operations (inside BuildPart)
180
+
181
+ | Operation | Parameters | Description |
182
+ |-----------|-----------|-------------|
183
+ | `fillet(edges, radius)` | list of edges, radius | Round edges |
184
+ | `chamfer(edges, length)` | list of edges, length | Bevel edges |
185
+ | `extrude(amount)` | distance (mm) | Extrude pending sketch into solid |
186
+ | `revolve(amount)` | revolution angle (default 360) | Revolve pending sketch |
187
+ | `loft()` | — | Loft between pending sketches |
188
+ | `sweep(path)` | path (wire/line) | Sweep pending sketch along path |
189
+ | `mirror(about)` | Plane or Axis | Mirror about plane/axis |
190
+ | `offset(amount)` | distance (positive=outward) | Offset shell/faces |
191
+ | `split(keep)` | Plane or keep=Keep.TOP/BOTTOM | Split and keep half |
192
+ | `scale(factor)` | scale factor | Scale uniformly |
193
+ | `thicken(amount)` | thickness | Thicken a face into solid |
194
+
195
+ ### Hole Operations
196
+
197
+ | Operation | Parameters | Description |
198
+ |-----------|-----------|-------------|
199
+ | `Hole(radius, depth=None)` | radius, optional depth | Simple hole (through if no depth) |
200
+ | `CounterBoreHole(radius, depth, counter_bore_radius, counter_bore_depth)` | — | Socket-head screw hole |
201
+ | `CounterSinkHole(radius, counter_sink_radius, counter_sink_angle=82, depth=None)` | — | Flat-head screw hole |
202
+
203
+ Holes operate on the current workplane. Without `depth`, `Hole` goes through
204
+ the full part thickness.
205
+
206
+ ### 2D Operations (inside BuildSketch)
207
+
208
+ | Operation | Description |
209
+ |-----------|-------------|
210
+ | `fillet(edges, radius)` | Round 2D edges |
211
+ | `chamfer(edges, length)` | Bevel 2D edges |
212
+ | `offset(amount)` | Offset sketch outline |
213
+ | `mirror(about)` | Mirror about axis |
214
+ | `trim(edge)` | Trim at intersection |
215
+
216
+ ## Positioning: Location Contexts
217
+
218
+ Location contexts set the workplane for everything created inside them. They
219
+ are stacked: nesting adds transforms.
220
+
221
+ ### Locations — explicit positions
222
+
223
+ ```python
224
+ # Single position
225
+ with Locations((10, 5, 0)):
226
+ Hole(radius=2)
227
+
228
+ # Multiple positions (creates feature at each)
229
+ with Locations((10, 5, 0), (-10, 5, 0)):
230
+ Hole(radius=2)
231
+
232
+ # Using a face to set workplane
233
+ with Locations(bp.faces().sort_by(Axis.Z)[-1]):
234
+ Hole(radius=2)
235
+ ```
236
+
237
+ ### GridLocations — rectangular array
238
+
239
+ ```python
240
+ # 2x3 grid, 20mm X spacing, 15mm Y spacing
241
+ with GridLocations(20, 15, 2, 3):
242
+ Hole(radius=2)
243
+ ```
244
+
245
+ ### PolarLocations — circular array
246
+
247
+ ```python
248
+ # 6 holes on a 30mm bolt circle
249
+ with PolarLocations(30, 6):
250
+ Hole(radius=2)
251
+
252
+ # Start angle offset, angular range
253
+ with PolarLocations(30, 4, start_angle=45, angular_range=180):
254
+ Hole(radius=2)
255
+ ```
256
+
257
+ ### HexLocations — hex grid pattern
258
+
259
+ ```python
260
+ # Hex grid for weight reduction pockets
261
+ with HexLocations(8, 3, 4, align=(Align.CENTER, Align.MIN)):
262
+ Circle(radius=3, mode=Mode.SUBTRACT)
263
+ ```
264
+
265
+ ## Location Arithmetic
266
+
267
+ `Location` objects can be composed with `*` and created from tuples or
268
+ named args:
269
+
270
+ ```python
271
+ # Create and combine locations
272
+ loc = Location((10, 0, 0)) * Location((0, 5, 0)) # combined offset
273
+
274
+ # Rotation
275
+ rot_loc = Location((0, 0, 0), (90, 0, 0)) # 90 deg around X
276
+
277
+ # Move an existing object
278
+ moved_box = Box(10, 10, 5).moved(Location((0, 0, 20)))
279
+ ```
280
+
281
+ `.moved()` returns a new object at the given location. It does not mutate
282
+ the original.
283
+
284
+ ## Selecting Topology
285
+
286
+ Builders expose topology selectors: `bp.solids()`, `bp.faces()`, `bp.edges()`,
287
+ `bp.vertices()`. These return `ShapeList` objects with powerful filtering.
288
+
289
+ ### ShapeList Methods
290
+
291
+ ```python
292
+ edges = bp.edges()
293
+
294
+ # Filter by axis alignment (edges parallel to Z)
295
+ z_edges = edges.filter_by(Axis.Z)
296
+
297
+ # Filter by geometry type
298
+ lines = edges.filter_by(GeomType.LINE)
299
+ circles = edges.filter_by(GeomType.CIRCLE)
300
+
301
+ # Sort along axis (ascending)
302
+ sorted_by_z = edges.sort_by(Axis.Z)
303
+ highest = sorted_by_z[-1] # top edge
304
+ lowest_four = sorted_by_z[:4] # bottom 4 edges
305
+
306
+ # Sort by alternative criteria
307
+ by_radius = edges.sort_by(SortBy.RADIUS)
308
+ largest_circle = by_radius[-1]
309
+
310
+ # Group by axis (returns list of ShapeLists)
311
+ groups = edges.group_by(Axis.Z)
312
+ # groups[0] = edges with lowest Z, groups[-1] = highest Z
313
+
314
+ # Position-based filtering
315
+ specific = [e for e in edges
316
+ if abs(e.center().Z - 5.0) < 0.1]
317
+ ```
318
+
319
+ ### ShapeList Operators
320
+
321
+ ```python
322
+ # Comparison: select elements above/below a position along axis
323
+ high = edges > Axis.Z # edges with center.Z > 0
324
+ low = edges < Axis.Z # edges with center.Z < 0
325
+
326
+ # Shift: select neighbors by index offset
327
+ last4 = edges >> 4 # last 4 edges (from sorted order)
328
+ first4 = edges << 4 # first 4 edges
329
+
330
+ # Union: combine two ShapeLists
331
+ combined = z_edges | circle_edges
332
+
333
+ # Index: access by position
334
+ first = edges[0]
335
+ last = edges[-1]
336
+ ```
337
+
338
+ ### Selector Strategy for Fillet/Chamfer
339
+
340
+ ```python
341
+ # 1. Get all edges from the builder
342
+ edges = bp.edges()
343
+
344
+ # 2. Filter by geometry type
345
+ line_edges = edges.filter_by(GeomType.LINE)
346
+
347
+ # 3. Filter by axis (edges parallel to Z)
348
+ vertical = line_edges.filter_by(Axis.Z)
349
+
350
+ # 4. Sort and slice
351
+ sorted_v = vertical.sort_by(Axis.Z)
352
+ top_4 = sorted_v[-4:] # 4 highest vertical edges
353
+ bottom_4 = sorted_v[:4] # 4 lowest vertical edges
354
+
355
+ fillet(top_4, radius=3)
356
+ chamfer(bottom_4, length=1)
357
+ ```
358
+
359
+ For edges at a specific position (e.g., junction between two parts):
360
+ ```python
361
+ # Filter by center position with tolerance
362
+ junction = [e for e in bp.edges().filter_by(GeomType.LINE).filter_by(Axis.X)
363
+ if abs(e.center().Z - junction_z) < 0.1
364
+ and abs(e.center().Y - junction_y) < 0.1]
365
+ if junction:
366
+ fillet(junction, radius=r)
367
+ ```
368
+
369
+ ## Workplanes and Planes
370
+
371
+ `BuildSketch` and hole operations use the current workplane. The default is
372
+ Plane.XY (the XY plane at Z=0).
373
+
374
+ ```python
375
+ # Sketch on XZ plane (e.g., for a side profile)
376
+ with BuildSketch(Plane.XZ) as sk:
377
+ Rectangle(20, 10)
378
+ extrude(amount=5) # extrude along Y axis
379
+
380
+ # Sketch on a face
381
+ with BuildSketch(bp.faces().sort_by(Axis.Z)[-1]) as sk:
382
+ Circle(radius=5)
383
+ extrude(amount=3) # boss growing upward from top face
384
+
385
+ # Custom plane offset from XY
386
+ custom = Plane(origin=(0, 0, 10), x_dir=(1, 0, 0), z_dir=(0, 0, 1))
387
+ ```
388
+
389
+ Key planes: `Plane.XY`, `Plane.XZ`, `Plane.YZ`, `Plane(origin, x_dir, z_dir)`.
390
+
391
+ ## Sketch-to-Solid Workflow
392
+
393
+ ### Extrude
394
+
395
+ ```python
396
+ with BuildPart() as bp:
397
+ with BuildSketch():
398
+ Rectangle(30, 20)
399
+ Circle(radius=5, mode=Mode.SUBTRACT) # hole in sketch
400
+ extrude(amount=10) # 10mm thick, Z direction by default
401
+ result = bp.part
402
+ ```
403
+
404
+ ### Revolve
405
+
406
+ ```python
407
+ with BuildPart() as bp:
408
+ with BuildSketch(Plane.XZ) as sk:
409
+ # Profile on XZ plane, will revolve around Z axis
410
+ with Locations((15, 0)):
411
+ Rectangle(5, 10)
412
+ revolve(amount=360) # full revolution
413
+ result = bp.part
414
+ ```
415
+
416
+ ### Sweep
417
+
418
+ ```python
419
+ with BuildPart() as bp:
420
+ with BuildSketch():
421
+ Circle(radius=3)
422
+ with BuildLine() as ln:
423
+ # Sweep path
424
+ Spline([(0,0,0), (10,0,5), (20,0,0)])
425
+ sweep(path=ln.line)
426
+ result = bp.part
427
+ ```
428
+
429
+ ### Loft
430
+
431
+ ```python
432
+ with BuildPart() as bp:
433
+ # Bottom profile
434
+ with BuildSketch(Plane.XY):
435
+ Rectangle(20, 20)
436
+ # Top profile (offset plane)
437
+ with BuildSketch(Plane.XY.offset(15)):
438
+ Circle(radius=8)
439
+ loft()
440
+ result = bp.part
441
+ ```
442
+
443
+ ## Complete Patterns
444
+
445
+ ### L-Bracket
446
+
447
+ ```python
448
+ with BuildPart() as bp:
449
+ # Base plate on Z=0
450
+ add(Box(base_length, base_width, base_thickness,
451
+ align=(Align.CENTER, Align.CENTER, Align.MIN)))
452
+
453
+ # Vertical web at back edge, rising from top of base
454
+ add(Box(base_length, web_thickness, web_height,
455
+ align=(Align.CENTER, Align.MAX, Align.MIN))
456
+ .moved(Location((0, base_width / 2, base_thickness))))
457
+
458
+ # Fillet interior junction
459
+ junction = [e for e in bp.edges().filter_by(GeomType.LINE).filter_by(Axis.X)
460
+ if abs(e.center().Z - base_thickness) < 0.1
461
+ and abs(e.center().Y - (base_width/2 - web_thickness/2)) < 0.1]
462
+ if junction:
463
+ fillet(junction, radius=fillet_radius)
464
+
465
+ # Base mounting holes (from top face down)
466
+ with Locations((-hole_spacing/2, -margin, base_thickness)):
467
+ CounterBoreHole(radius=r, depth=d, counter_bore_radius=cr, counter_bore_depth=cd)
468
+ with Locations((hole_spacing/2, -margin, base_thickness)):
469
+ CounterBoreHole(radius=r, depth=d, counter_bore_radius=cr, counter_bore_depth=cd)
470
+
471
+ # Web mounting holes (from outer face inward)
472
+ for z in (base_thickness + margin_z, base_thickness + margin_z + spacing_z):
473
+ with Locations((0, base_width, z)):
474
+ CounterBoreHole(radius=r, depth=d, counter_bore_radius=cr, counter_bore_depth=cd)
475
+
476
+ result = bp.part
477
+ ```
478
+
479
+ ### Tube / Pipe
480
+
481
+ ```python
482
+ with BuildPart() as bp:
483
+ add(Cylinder(radius=outer_r, height=length,
484
+ align=(Align.CENTER, Align.CENTER, Align.MIN)))
485
+ with Locations((0, 0, length)): # workplane at top
486
+ Hole(radius=inner_r) # through-hole
487
+ result = bp.part
488
+ ```
489
+
490
+ ### Flange with Bolt Pattern
491
+
492
+ ```python
493
+ with BuildPart() as bp:
494
+ add(Box(flange_w, flange_w, flange_h,
495
+ align=(Align.CENTER, Align.CENTER, Align.MIN)))
496
+ # Center bore
497
+ with Locations((0, 0)):
498
+ Hole(radius=bore_r)
499
+ # Corner bolt holes
500
+ with GridLocations(spacing, spacing, 2, 2):
501
+ Hole(radius=screw_r)
502
+ result = bp.part
503
+ ```
504
+
505
+ ### Swept Profile (Duct)
506
+
507
+ ```python
508
+ with BuildPart() as bp:
509
+ with BuildSketch():
510
+ Rectangle(width, height)
511
+ with BuildLine() as path:
512
+ Line((0, 0), (0, duct_length))
513
+ sweep(path=path.line)
514
+ result = bp.part
515
+ ```
516
+
517
+ ## Operation Order
518
+
519
+ The order of operations inside BuildPart matters critically:
520
+
521
+ 1. **Add base solid** — `add(Box(...))` or `add(Cylinder(...))`
522
+ 2. **Fillet/chamfer structural edges** — before boolean cuts that change topology
523
+ 3. **Subtract features** — holes, pockets, CounterBoreHole
524
+ 4. **Add bosses** — cylinders, extruded sketches
525
+
526
+ ```python
527
+ # WRONG: fillet after hole picks wrong edges
528
+ with BuildPart() as bp:
529
+ add(Box(30, 30, 10))
530
+ Hole(radius=5) # changes edge topology
531
+ fillet(bp.edges()[-4:], radius=2) # -4 picks wrong edges now
532
+
533
+ # RIGHT: fillet before subtracting
534
+ with BuildPart() as bp:
535
+ add(Box(30, 30, 10))
536
+ fillet(bp.edges().filter_by(Axis.Z)[-4:], radius=2)
537
+ Hole(radius=5)
538
+ ```
539
+
540
+ ## Coordinate Convention
541
+
542
+ - +X right, +Y back, +Z up
543
+ - Origin (0,0,0) is center of the default workspace
544
+ - `Align.MIN` = negative side, `Align.CENTER` = centered, `Align.MAX` = positive side
545
+
546
+ ## Params Pattern
547
+
548
+ ```python
549
+ import json
550
+ from pathlib import Path
551
+
552
+ PARAMS = json.loads(
553
+ Path(__file__).with_name("params.json").read_text(encoding="utf-8")
554
+ )
555
+ length = float(PARAMS["length"])
556
+ width = float(PARAMS["width"])
557
+ height = float(PARAMS["height"])
558
+ ```
559
+
560
+ Load params at module top level. Do not hardcode dimensions in geometry code.
561
+
562
+ ## Common Pitfalls
563
+
564
+ ### 9. ⚠️ CRITICAL: Locations does NOT shift BuildSketch plane in Z
565
+
566
+ `Locations((x, y, z))` works perfectly for **3D primitives** (Box, Cylinder, Cone)
567
+ but does **NOT** relocate the plane of `BuildSketch`. The sketch always materialises
568
+ on the plane you pass to `BuildSketch(...)`, regardless of any enclosing `Locations`.
569
+
570
+ ```python
571
+ # WRONG — inner cavity will start at Z=0, not at Z=wall_back
572
+ with Locations((0, 0, wall_back)):
573
+ with BuildSketch(Plane.XY): # still Z=0 globally
574
+ RectangleRounded(w, h, r)
575
+ extrude(amount=depth, mode=Mode.SUBTRACT)
576
+
577
+ # CORRECT — use an explicit Plane with the desired origin
578
+ with BuildSketch(Plane(origin=(0, 0, wall_back))):
579
+ RectangleRounded(w, h, r)
580
+ extrude(amount=depth, mode=Mode.SUBTRACT)
581
+
582
+ # ALSO CORRECT — for off-axis features combine X/Y/Z in the origin
583
+ cam_cx, cam_cy = -10.3, 53.3
584
+ with BuildSketch(Plane(origin=(cam_cx, cam_cy, -0.1))):
585
+ RectangleRounded(cam_w, cam_h, cam_r)
586
+ extrude(amount=wall_back + 0.2, mode=Mode.SUBTRACT)
587
+ ```
588
+
589
+ **Rule of thumb:**
590
+ - `Locations` → use it only with 3D primitives (Box, Cylinder, Cone, Sphere).
591
+ - `BuildSketch` → always pass the plane explicitly: `Plane(origin=(x, y, z))`.
592
+
593
+ Symptom of the bug: a feature that should start at Z>0 (e.g., an inner cavity)
594
+ instead starts at Z=0, obliterating the back wall. Or a cutout that should remove
595
+ material from a back panel removes nothing because the back panel never existed.
596
+
597
+ Diagnostic: intersect the part with a probe `Box(w, h, ε)` at the expected Z
598
+ height and check the resulting volume. Zero volume means no material at that Z.
599
+
600
+ ### 1. Fillet radius too large
601
+ If the radius exceeds the edge length or adjacent face width, the boolean fails
602
+ with a topology error. Reduce radius.
603
+
604
+ ### 2. Zero-thickness geometry
605
+ Two faces exactly coplanar causes boolean failures. Add a 0.01mm gap if needed.
606
+
607
+ ### 3. Workplane confusion
608
+ BuildSketch defaults to Plane.XY (Z=0). Holes go in the -Z direction from the
609
+ current workplane. If a hole doesn't go through, check the workplane location.
610
+ Use `Plane(origin=(x, y, z))` to place a sketch at an arbitrary position in space
611
+ (see pitfall #9 above — never rely on Locations for this).
612
+
613
+ ### 4. Nested builders don't inherit workplanes
614
+ ```python
615
+ # BuildSketch inside BuildPart does NOT inherit BuildPart's workplane
616
+ with BuildPart() as bp:
617
+ add(Box(50, 50, 10, align=(Align.CENTER, Align.CENTER, Align.MIN)))
618
+ with BuildSketch(): # still Plane.XY, NOT top of box
619
+ Rectangle(5, 5)
620
+ extrude(amount=3) # extrudes from Z=0 upward
621
+ ```
622
+
623
+ To sketch on top of the box:
624
+ ```python
625
+ with BuildSketch(bp.faces().sort_by(Axis.Z)[-1]):
626
+ Rectangle(5, 5)
627
+ extrude(amount=3)
628
+ ```
629
+
630
+ ### 5. Self-intersection
631
+ Objects that intersect themselves (e.g., a wall with zero thickness where two
632
+ sides meet) create invalid BREP. Ensure all solid walls have material thickness.
633
+
634
+ ### 6. ShapeList slicing vs single edge
635
+ `edges.sort_by(Axis.Z)[-1]` returns a single Edge. `edges.sort_by(Axis.Z)[-2:]`
636
+ returns a ShapeList. Both work with `fillet()`. But `[-0:]` is wrong — use `[-1]`.
637
+
638
+ ### 7. Cylinder rotation
639
+ `Cylinder` grows along its local Z axis. To orient it along X or Y, use
640
+ `rotation=(90, 0, 0)` to rotate around X, or `rotation=(0, 90, 0)` to rotate
641
+ around Y.
642
+
643
+ ### 8. ⚠️ CRITICAL: `Box(...).moved()` inside BuildPart double-adds
644
+
645
+ Inside a `BuildPart` context, `Box(...)` is **immediately added to the part
646
+ at its construction location**. Calling `.moved(...)` returns a NEW object,
647
+ but the original `Box(...)` was already added. If you then `add()` the moved
648
+ copy, the part contains the box twice — once at the original location and
649
+ once at the moved location.
650
+
651
+ ```python
652
+ # ❌ WRONG — Box appears twice in the final part!
653
+ with BuildPart() as bp:
654
+ Box(50, 4, 30).moved(Location((0, 18, 15))) # Box first added at origin,
655
+ # then moved-copy is discarded
656
+ # OR equally wrong:
657
+ web = Box(50, 4, 30, align=...).moved(Location((0, 18, 15)))
658
+ add(web) # original Box already added at origin; this adds it again at (0,18,15)
659
+
660
+ # ✅ CORRECT — use Locations to position the Box during creation
661
+ with BuildPart() as bp:
662
+ with Locations((0, 18, 15)):
663
+ Box(50, 4, 30)
664
+ ```
665
+
666
+ Rule: **inside a builder, position objects with `Locations` during creation,
667
+ not by `.moved()` afterwards.** `.moved()` is safe only on standalone Shape
668
+ objects you build outside a builder context.
669
+
670
+ `agentcad probe <model> --scan --axis y --json` is the fastest way to detect this
671
+ bug — you will see a doubled mass distribution along the moved axis.
@@ -0,0 +1,3 @@
1
+ # References
2
+
3
+ Put images, sketches, scan files, and user notes for this CAD project here.