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.
- agentcad/__init__.py +3 -0
- agentcad/__main__.py +4 -0
- agentcad/_templates/__init__.py +0 -0
- agentcad/_templates/model/README.md +46 -0
- agentcad/_templates/model/design.json +43 -0
- agentcad/_templates/model/params.json +5 -0
- agentcad/_templates/model/part.py +33 -0
- agentcad/_templates/workspace/CLAUDE.md +420 -0
- agentcad/_templates/workspace/cadproject.json +7 -0
- agentcad/_templates/workspace/models/.gitkeep +0 -0
- agentcad/_templates/workspace/references/build123d-guide.md +671 -0
- agentcad/_templates/workspace/references/images/.gitkeep +0 -0
- agentcad/_templates/workspace/references/notes.md +3 -0
- agentcad/_templates/workspace/references/validation-strategy.md +378 -0
- agentcad/checks/__init__.py +130 -0
- agentcad/checks/mesh.py +131 -0
- agentcad/checks/relations.py +155 -0
- agentcad/checks/section.py +199 -0
- agentcad/cli.py +235 -0
- agentcad/contract.py +160 -0
- agentcad/geometry.py +417 -0
- agentcad/inspect.py +102 -0
- agentcad/jsonio.py +30 -0
- agentcad/measure.py +80 -0
- agentcad/payloads.py +24 -0
- agentcad/precheck.py +153 -0
- agentcad/probe.py +215 -0
- agentcad/render.py +166 -0
- agentcad/report.py +151 -0
- agentcad/review.py +399 -0
- agentcad/runner.py +182 -0
- agentcad/section.py +346 -0
- agentcad/stl.py +265 -0
- agentcad/templates.py +21 -0
- agentcad/validate.py +234 -0
- agentcad/workspace.py +173 -0
- agentcad_cli-0.1.1.dist-info/METADATA +145 -0
- agentcad_cli-0.1.1.dist-info/RECORD +41 -0
- agentcad_cli-0.1.1.dist-info/WHEEL +5 -0
- agentcad_cli-0.1.1.dist-info/entry_points.txt +2 -0
- 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.
|
|
File without changes
|