@protoboardai/ext-enclosure-cad 0.0.0-stage → 0.2.0
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.
- package/LICENSE +202 -0
- package/NOTICE +13 -0
- package/README.md +388 -2
- package/evals/fit-grown-board/case.ts +71 -0
- package/lib/check.ts +379 -0
- package/lib/features.ts +424 -0
- package/lib/index.ts +8 -0
- package/lib/job.ts +388 -0
- package/lib/pyjson.ts +68 -0
- package/lib/standards.ts +302 -0
- package/ops/cad.ts +264 -0
- package/ops/package.ts +213 -0
- package/package.json +41 -4
- package/protoboard-extension.json +69 -0
- package/py/enclosure_cad.py +1731 -0
- package/py/fit.py +341 -0
- package/skills/enclosure-cad/SKILL.md +194 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Protoboard Stack: @protoboardai/ext-enclosure-cad
|
|
2
|
+
Copyright 2026 Delta Robotics, Inc.
|
|
3
|
+
|
|
4
|
+
This product includes software developed at Delta Robotics, Inc.
|
|
5
|
+
(https://deltaroboticsinc.com).
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License").
|
|
8
|
+
See the LICENSE file in this package for full terms.
|
|
9
|
+
|
|
10
|
+
"Protoboard", the Protoboard logo and the "0" mark are trademarks of
|
|
11
|
+
Delta Robotics, Inc. The License grants no rights to them (section 6);
|
|
12
|
+
see TRADEMARKS.md in the Protoboard Stack repository
|
|
13
|
+
(https://github.com/Delta-Robotics-Inc/protoboard-stack).
|
package/README.md
CHANGED
|
@@ -1,3 +1,389 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @protoboardai/ext-enclosure-cad
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A `cad` capability provider for enclosures: cases, caps and back covers.
|
|
4
|
+
It generates a printed or machined enclosure with build123d from two things
|
|
5
|
+
the project already knows:
|
|
6
|
+
|
|
7
|
+
- **the features the enclosure must provide**, as the interfaces of its UHD
|
|
8
|
+
module (an opening for a USB-C receptacle, plungers for two switches, an
|
|
9
|
+
O-ring groove, strap lugs, …);
|
|
10
|
+
- **what goes inside it**, from the assembly: the board's standard outputs,
|
|
11
|
+
the cell, the display, every other placed body.
|
|
12
|
+
|
|
13
|
+
It then exports the `cad` standard outputs, extracts the feature frames, and
|
|
14
|
+
checks the design against the model and against what it holds. The contract
|
|
15
|
+
it implements is in [tool integrations](../../docs/specs/tool-integrations.md)
|
|
16
|
+
and the [provider guide](../../docs/guides/writing-a-capability-provider.md).
|
|
17
|
+
|
|
18
|
+
The skill **enclosure-cad** (`skills/enclosure-cad/SKILL.md`) is the
|
|
19
|
+
procedure an agent follows; this README is its reference.
|
|
20
|
+
|
|
21
|
+
## The model
|
|
22
|
+
|
|
23
|
+
An enclosure is an ordinary UHD module. Each feature is a mechanical
|
|
24
|
+
interface carrying an `enclosure_feature` trait: its kind, its sizes in mm,
|
|
25
|
+
the standard part it takes, and, for features that give access to something
|
|
26
|
+
inside, the part it serves. The builders in `lib/features.ts` write these
|
|
27
|
+
interfaces; the data they write names no tool, so another provider (Onshape,
|
|
28
|
+
Zoo) reads the same module.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
// src/enclosures/case/case.uhd.ts
|
|
32
|
+
import { Button, InsertBosses, Joint, Opening, M2X6_PAN, RUTHEX_RX_M2X4, USB_C_PLUG } from "@protoboardai/ext-enclosure-cad/lib/index.ts";
|
|
33
|
+
|
|
34
|
+
export const CASE = {
|
|
35
|
+
id: "watch-case",
|
|
36
|
+
name: "Watch case",
|
|
37
|
+
interfaces: [
|
|
38
|
+
Opening({ id: "usb_opening", serves: { child: "board", interfaceId: "usb" }, widthMm: 9.6, heightMm: 3.9, cornerRadiusMm: 1.2, maxRecessMm: 2.0, plug: USB_C_PLUG, assumption: "…" }),
|
|
39
|
+
Button({ id: "btn_a", serves: { child: "board", interfaceId: "btn_a" }, holeDiameterMm: 2.5, travelMm: 0.2 }),
|
|
40
|
+
InsertBosses({ id: "board_mount", shape: "rectangle", spacingMm: 25, spacingYmm: 19, holeCount: 4, fastener: "M2", fastenerDiameterMm: 2, insert: RUTHEX_RX_M2X4, frame: { origin: [0, 0, 6], normal: [0, 0, 1] } }),
|
|
41
|
+
Joint({ id: "lid_joint", screw: M2X6_PAN, insert: RUTHEX_RX_M2X4, count: 4 }),
|
|
42
|
+
],
|
|
43
|
+
traits: [{ type: "design_envelope", params: { max_mm: { length: 50, width: 45, height: 20 } } }],
|
|
44
|
+
};
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`serves` names an interface of a sibling module in the system the enclosure
|
|
48
|
+
sits in. The feature is placed on that interface's axis and checked against
|
|
49
|
+
it. The frame comes from the served module's geometry manifest when it has
|
|
50
|
+
one (a circuit provider's `board.manifest.json` names every connector, with
|
|
51
|
+
its normal), else from the interface's UHD frame. This relation is a
|
|
52
|
+
candidate for a UHD link kind; until UHD has one it is a trait parameter.
|
|
53
|
+
|
|
54
|
+
### Features
|
|
55
|
+
|
|
56
|
+
| Kind | Builder | The generator makes | The check verifies |
|
|
57
|
+
| --- | --- | --- | --- |
|
|
58
|
+
| `opening` | `Opening` | A rectangular (rounded) or round cut through the wall where the served part's axis leaves the shell | Axis offset ≤ 0.3 mm and ≤ 2° from the served part; recess ≤ `max_recess_mm`; what each part keeps above and below the cut; with `plug` (`USB_C_PLUG`), that the plug passes and its overmold has a flat seat (clear of the case edges and the screw columns' bulges) |
|
|
59
|
+
| `button` | `Button` | A bore, and a separate printed plunger: shaft to `gap_mm` short of the actuator, `protrude_mm` outside, and a flange behind the wall, trimmed (a polygon around the axis) to keep the design clearance from the walls and from every body inside | Alignment; gap ≥ 0 (not pressed at rest) and gap + switch travel ≤ protrusion (it can press the switch); room for the flange; after trimming, the flange overlaps the wall by ≥ 0.5 mm over ≥ half its edge (assumption: enough that it cannot tilt out through the bore); the plunger intersects no wall (exact) and no body inside (voxel) |
|
|
60
|
+
| `window` | `Window` | An aperture and a pocket for the cover (glass, lens, IR window), outside (flush) or inside | Alignment; the ledge holding the cover and the wall left under the pocket |
|
|
61
|
+
| `light_pipe` | `LightPipe` | A hole for the pipe plus clearance | Alignment; edge margins |
|
|
62
|
+
| `boss` | `InsertBosses` | Bosses from the floor to the frame (a structure bolt pattern), with heat-set insert holes; the floor is lowered if the insert hole needs it | Boss wall ≥ the insert's minimum wall; skin under the hole; the board mated to them sits on them (contact there is by design) |
|
|
63
|
+
| `seal_gland` | `SealGland` | Face groove in the base rim at the lid joint (`at: "joint"`, flat joint), a radial groove round a twist lid's collar (radial, `at: "joint"`), a face groove ring at a frame, or a static radial (female) gland around a rod or tube, with a boss when the wall is too thin | Squeeze within the gland table's range; installed stretch ≤ 5 %; face groove within +1 % of the O-ring's I.D. (external pressure) or −1 % of its O.D. (internal); rim ledges |
|
|
64
|
+
| `thread` | `Thread` | An internal single-start thread in a skirt under the floor (a cap on a neck) | Skirt wall; turns of engagement are reported |
|
|
65
|
+
| `strap_lugs` | `StrapLugs` | Two lugs at the ±Y end with blind spring-bar holes; the bar clears the case by `strap_gap_mm` at the strap's centre line | Lug wall and edge beyond the holes |
|
|
66
|
+
| `joint` | `Joint` | The split into base and lid: a rabbet (tongue on the base, skirt on the lid, each (wall − joint clearance) / 2 wide) or a flat joint; with screws, four columns with insert holes and lid clearance holes | Tongue and skirt widths; column wall around the insert; the screw does not bottom out |
|
|
67
|
+
| `twist_lid` | `TwistLid` | A lid that closes without screws on a round shell: the base's wall steps in to a collar at the top, the lid's skirt (the shell's own outer diameter) slides over it and locks. `lock: "bayonet"`: lugs low on the collar, 45° underneath, into L-slots in the skirt whose lower faces hold the lid down; the slot ends fix the lid's angle, and a key lug (`keyLugWidthMm`, the lug on +Y wider than a slot for the others) lets the lid go on in one position only, so features in the lid (light pipes) stand over what they serve. Equal lugs let it go on in as many positions as there are lugs: the build refuses that unless `orientation: "any"`. `lock: "thread"`: an external thread on the collar, the lid's internal one offset by half a pitch, 30° flanks. With a `SealGland` radial `at: "joint"`, an O-ring groove round the collar (above the lugs, or under the thread, so the skirt's bore that seals it is never slotted or threaded), and a 19° lead-in on the bore. `boreAllowanceMm` draws the bore that much (radial) over the size it should print at, for a printed bore's undersize; the groove's depth is from the printed bore. Facts: radii, levels, lug angles and widths, the positions the lid goes on in (`lid_positions`), or the thread, the drawn and printed bore, and each part's print orientation | Base wall behind the groove and at the lock; skirt wall behind the slots or the thread (≥ minimum wall); a bayonet's lid goes on one way (or `orientation: "any"`), its key wider than a slot for the others, and the skirt between neighbouring slots ≥ minimum wall; the collar fits above the floor; no opening, window or button cuts the side wall where the skirt covers the collar (the lid turns); the seal's squeeze against Parker's radial chart (the collar off centre by the radial clearance) and the groove's stretch of the O-ring (0-5 %); with a bore allowance, the squeeze if the bore prints at its drawn size (info) |
|
|
68
|
+
|
|
69
|
+
The standard parts these take are cited in `lib/standards.ts`:
|
|
70
|
+
|
|
71
|
+
| Data | Source |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| AS568 O-ring sizes (I.D., cross-section, tolerances) | Parker O-Ring Handbook ORD 5700A/US (2001), Series 2-XXX sizes, metric columns |
|
|
74
|
+
| Face seal glands | Parker ORD 5700 Design Chart 4-2 |
|
|
75
|
+
| Static radial glands | Parker ORD 5700 Design Chart 4-1 |
|
|
76
|
+
| 5 % maximum installed stretch | Parker ORD 5700 §3.3 |
|
|
77
|
+
| Face groove sized by the O-ring's I.D. (+1 %) or O.D. (−1 %) | Parker ORD 5700 Design Chart 4-2 notes |
|
|
78
|
+
| ruthex RX-M2x4 insert (Ø3.6 × 4.0, hole Ø3.2, wall ≥ 1.3) | ruthex product data, as read by the watch and bottle projects |
|
|
79
|
+
| CNC Kitchen M2 insert (Ø3.6 × 3.0, hole Ø3.2, wall ≥ 1.3) | 3DJake product page |
|
|
80
|
+
| M2 × 6 pan head screw; Ø2.4 clearance hole | Accu SPP-M2-6-A2 data; ISO 273 medium series |
|
|
81
|
+
| Esslinger 82.2225 spring bar (Ø1.5, tips Ø0.8, 25.0 overall, 21.8 compressed) | Esslinger product page |
|
|
82
|
+
| USB Type-C plug, `USB_C_PLUG` (overmold 12.35 × 6.5 max, plug 8.25 × 2.40, length 6.65) | USB Type-C Compliance Document rev 1.2, Appendix B, Figure B-1 |
|
|
83
|
+
|
|
84
|
+
Assumptions are stated where they are made: insert hole depth (insert length
|
|
85
|
+
+ 1 mm), lug hole size (tip + 0.2 mm, tip length + 0.3 mm), the minimum wall
|
|
86
|
+
by process (print 0.8 mm: two 0.4 mm extrusions; machine 0.5 mm), the
|
|
87
|
+
alignment tolerance (0.3 mm, 2°), the plunger flange's hold (0.5 mm over
|
|
88
|
+
half its edge). A module states its own values where it knows better, with a
|
|
89
|
+
`source` or an `assumption`.
|
|
90
|
+
|
|
91
|
+
### Cuts across the lid joint
|
|
92
|
+
|
|
93
|
+
A cut through a side wall (an opening, a bore, a pocket) is checked part by
|
|
94
|
+
part, across the wall in bands: for a rabbet, the lid's skirt outside, the
|
|
95
|
+
joint clearance, and the base's tongue inside, each with the z span the base
|
|
96
|
+
and the lid hold there. What each part keeps above and below the cut, in each
|
|
97
|
+
band, is a wall. Where a cut leaves a sliver thinner than the minimum wall at
|
|
98
|
+
the joint inside the wall (the tongue over a USB opening that stops just
|
|
99
|
+
under its top, say), the generator cuts the sliver away: the other part
|
|
100
|
+
closes the gap there and nothing shows from outside (`margins_mm.cleared` in
|
|
101
|
+
the report). A sliver on the outer face, or at the top or bottom of the case,
|
|
102
|
+
is left as designed and reported as `ENCLOSURE_THIN_WALL`.
|
|
103
|
+
|
|
104
|
+
A rabbet needs a wall of at least two minimum walls plus the joint clearance
|
|
105
|
+
(1.7 mm at 0.8 mm and 0.1 mm); a thinner one is reported.
|
|
106
|
+
|
|
107
|
+
### What goes inside
|
|
108
|
+
|
|
109
|
+
The job (`lib/job.ts`) assembles the system the way the viewer and
|
|
110
|
+
ext-harness-cad do (UHD `assemble` with the project's display placements;
|
|
111
|
+
`projectAssembly` is imported from ext-harness-cad) and takes every placed
|
|
112
|
+
body except:
|
|
113
|
+
|
|
114
|
+
- the enclosure itself;
|
|
115
|
+
- what its outside features hold (a bottle on the thread, spring bars in the lugs);
|
|
116
|
+
- instance paths listed in `exclude`;
|
|
117
|
+
- a part already inside a placed parent's body (a board's components).
|
|
118
|
+
|
|
119
|
+
A module designed by a capability provider contributes its standard output
|
|
120
|
+
(`out/board.glb` for a board); any other module its body GLB artifact. A
|
|
121
|
+
placed module with no GLB is reported, because the fit check cannot see it.
|
|
122
|
+
Fastener hardware is not included yet.
|
|
123
|
+
|
|
124
|
+
## Configuration
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
// src/enclosures/case/cad.config.ts
|
|
128
|
+
import { defineCapabilityConfig } from "@protoboardai/core";
|
|
129
|
+
|
|
130
|
+
export default defineCapabilityConfig({
|
|
131
|
+
provider: "@protoboardai/ext-enclosure-cad",
|
|
132
|
+
process: "print",
|
|
133
|
+
constraints: { wall_mm: 1.7, clearance_mm: 0.3, material: "PETG", fasteners: "screws", joint: { style: "rabbet", clearance_mm: 0.1 } },
|
|
134
|
+
});
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| Constraint | Default | Meaning |
|
|
138
|
+
| --- | --- | --- |
|
|
139
|
+
| `wall_mm` | required | Side wall thickness |
|
|
140
|
+
| `floor_mm`, `lid_mm` | `wall_mm` | Floor and lid thickness |
|
|
141
|
+
| `clearance_mm` | 0.3 | Gap between what goes inside and the walls |
|
|
142
|
+
| `clearance_top_mm` | `clearance_mm` | Gap above the tallest part |
|
|
143
|
+
| `min_wall_mm` | print 0.8, machine 0.5 | Thinnest wall allowed anywhere |
|
|
144
|
+
| `material` | — | Recorded in the manifest (mass = volume × density) |
|
|
145
|
+
| `shape` | `box` | `box` (rounded rectangle) or `round` |
|
|
146
|
+
| `cavity_mm` | sized from the contents | Fixed cavity: `{ min, max }` (box) or `{ centre, radius, z }` (round) |
|
|
147
|
+
| `split_z_mm` | under the lid | Split plane between base and lid |
|
|
148
|
+
| `joint` | rabbet, 1.5 mm lip, 0.15 mm clearance | `{ style: "rabbet" \| "flat" \| "twist", lip_height_mm, clearance_mm }`; a joint O-ring makes it flat, a `TwistLid` twist (`fasteners: "none"`). The tongue and the skirt are each (wall − clearance) / 2 wide |
|
|
149
|
+
| `fasteners` | `screws` if a joint states screws | `screws` or `none` |
|
|
150
|
+
| `exclude` | — | Placed instance paths that are not inside |
|
|
151
|
+
| `resolution_mm` | 0.25 | Voxel size of the fit analysis |
|
|
152
|
+
|
|
153
|
+
`process` decides the manufacturing exports: `print` gives an STL and a 3MF
|
|
154
|
+
per printed part (the report marks the parts `printed`), `machine` the STEP
|
|
155
|
+
alone. `cut` is refused: cut panels are
|
|
156
|
+
a plate provider's job (ext-frame-cad).
|
|
157
|
+
|
|
158
|
+
## Operations and files
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
src/enclosures/case/
|
|
162
|
+
case.uhd.ts the enclosure as a UHD module (its features)
|
|
163
|
+
cad.config.ts this provider and its constraints
|
|
164
|
+
build123d/ native design (provider-owned)
|
|
165
|
+
fixture-case.job.json the generator's inputs: contents placed in the case's coordinates, features, design values
|
|
166
|
+
fixture-case.py the build script; runs the generator on the job
|
|
167
|
+
fixture-case.report.json what the build found: cavity, parts, feature facts, walls, problems, fit
|
|
168
|
+
fixture-case.source.json digests of the job and script the build came from
|
|
169
|
+
build/ what the script built: parts/<part>.step|glb|stl|3mf, body.step, body.glb,
|
|
170
|
+
body.manifest.json, section.svg (a cache: export rebuilds it)
|
|
171
|
+
out/ standard outputs (export)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Commit the native design (job, script, report, source) and `out/`. The
|
|
175
|
+
repository ignores `build/` directories; `check` needs only the report, the
|
|
176
|
+
job and, to re-analyse the fit when what goes inside has changed, the parts
|
|
177
|
+
in `out/body.glb` (one node per part), so a fresh clone is checked without
|
|
178
|
+
rebuilding.
|
|
179
|
+
|
|
180
|
+
| Operation | What it does |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `cad:generate` | Builds the job from the model, the assembly and the config; writes the job and the script; runs the script; writes the report and the source digests next to the job, and the parts and body under `build/` |
|
|
183
|
+
| `cad:export` | Copies `body.step`, `body.glb`, `body.manifest.json`, `section.svg` and, for print, `<part>.stl` and `<part>.3mf` into `out/`. If the job or the script was edited since the last build, it rebuilds from them first |
|
|
184
|
+
| `cad:extract` | Returns the generated frame of every feature, keyed by interface id, and the artifacts the module should reference (`cad_source`, `cad_step`, `cad_glb`, `cad_manifest` (the ids the other cad providers use), `<part>_stl`, `<part>_3mf`) |
|
|
185
|
+
| `cad:check` | Pure. Drift, fit, walls, seals, alignment, envelope (below) |
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx protoboard tools install cad # once: the cad runtime it shares with ext-frame-cad and ext-harness-cad
|
|
189
|
+
npx protoboard run cad:generate --module case # PROTOBOARD_PYTHON=<python> overrides the runtime
|
|
190
|
+
npx protoboard run cad:export --module case
|
|
191
|
+
npx protoboard run cad:extract --module case --format json
|
|
192
|
+
npx protoboard check
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`body.manifest.json` is a UHD geometry manifest: one feature per interface
|
|
196
|
+
id (area, centroid, normal, face count, from the faces the feature's tool
|
|
197
|
+
made), the volume of each printed part, and the generated frames. The STEP
|
|
198
|
+
holds each part and one thin named sub-shape per feature, so a UHD
|
|
199
|
+
`feature("<id>", { … })` reference can bind to it.
|
|
200
|
+
|
|
201
|
+
Generation is deterministic for a fixed job and build123d version: STEP
|
|
202
|
+
timestamps, 3MF entry times and 3MF object UUIDs are normalised.
|
|
203
|
+
|
|
204
|
+
**Hand edits.** The job and the script are the native design. Edit them to
|
|
205
|
+
change the enclosure directly; `export` rebuilds from them, and `check`
|
|
206
|
+
reports `ENCLOSURE_STALE` (the design no longer matches the model) and
|
|
207
|
+
`ENCLOSURE_NATIVE_EDITED` (not rebuilt yet). `generate` writes both files
|
|
208
|
+
again from the model.
|
|
209
|
+
|
|
210
|
+
## Checks
|
|
211
|
+
|
|
212
|
+
The fit analysis (`py/fit.py`) samples every body inside on its surface and
|
|
213
|
+
looks the samples up two ways:
|
|
214
|
+
|
|
215
|
+
- **Exact**, against the primitives the generator built the inside from: the
|
|
216
|
+
cavity (a box or a cylinder) and the solids standing in it (insert bosses,
|
|
217
|
+
lid screw columns, radial-gland bosses; finite cylinders). Exact on flat
|
|
218
|
+
faces, good to the sampling step (half a voxel) on curved edges. No
|
|
219
|
+
tolerance: below 0 mm is a collision, below the design clearance is tight.
|
|
220
|
+
- **Voxel**, in the signed clearance field of the parts as built, from
|
|
221
|
+
ext-harness-cad's obstacle map (`py/route.py`) and GLB reader
|
|
222
|
+
(`py/glb.py`), imported from that package: the parts are voxelised and
|
|
223
|
+
filled, the distance transform of free space is the clearance field the
|
|
224
|
+
harness router uses, and the same grid gives the depth of points inside a
|
|
225
|
+
wall. This sees what the primitives do not (a hand-edited script). It is
|
|
226
|
+
good to about one cell, so it has a one-cell band: a collision only past one
|
|
227
|
+
cell of penetration, tight only one cell under the clearance.
|
|
228
|
+
|
|
229
|
+
The report says which analysis found the worst point (`basis`), and so do the
|
|
230
|
+
messages ("exact", or "±0.25 mm voxels"). Containment in the cavity is exact
|
|
231
|
+
too (mesh vertices against the box or cylinder). The same grid gives a voxel
|
|
232
|
+
estimate of the local wall thickness (twice the depth on the solid's medial
|
|
233
|
+
surface); the exact per-feature walls come from the generator.
|
|
234
|
+
|
|
235
|
+
Button plungers go through a wall by design, so they are checked apart:
|
|
236
|
+
against the walls exactly (the plunger's solid intersected with the base and
|
|
237
|
+
the lid), and against every body inside in a voxel field of the contents
|
|
238
|
+
around the plunger (its tip, which meets the switch by design, is left out;
|
|
239
|
+
the tip gap is checked exactly from the served frame).
|
|
240
|
+
|
|
241
|
+
When what goes inside has changed since generation, `check` re-runs the fit
|
|
242
|
+
analysis on the current bodies against the parts as built (from
|
|
243
|
+
`out/body.glb`, else `build/parts/`; the spec goes to Python on stdin;
|
|
244
|
+
nothing is written). Otherwise it reads the results recorded at generation,
|
|
245
|
+
and needs no Python.
|
|
246
|
+
|
|
247
|
+
Seal squeeze is checked against the row of Parker's chart for the O-ring's
|
|
248
|
+
cross-section (`lib/standards.ts`), not the copy of it in the job, so a hand
|
|
249
|
+
edit to the job's table is caught. The squeeze uses the gap the joint leaves
|
|
250
|
+
and the message states its basis: a flat joint with lid screws closes face to
|
|
251
|
+
face (contact); a face gland at a frame (a bottle lip on a cap) assumes the
|
|
252
|
+
mating face bears on it, because what presses it (the thread's engagement) is
|
|
253
|
+
not modelled; a radial gland's rod may sit off centre by the radial
|
|
254
|
+
clearance, so its squeeze is checked over that range.
|
|
255
|
+
|
|
256
|
+
| Code | Severity | When |
|
|
257
|
+
| --- | --- | --- |
|
|
258
|
+
| `ENCLOSURE_NOT_GENERATED` | info | No generated design yet |
|
|
259
|
+
| `ENCLOSURE_FEATURE_INVALID` | error | A feature interface the provider cannot read |
|
|
260
|
+
| `ENCLOSURE_INPUT` | error | The model does not say enough (a served part with no frame, the enclosure not placed, …) |
|
|
261
|
+
| `ENCLOSURE_STALE` | warning | Contents, features, design values or the job changed since generation; lists what |
|
|
262
|
+
| `ENCLOSURE_NATIVE_EDITED` | info | The native design was edited after its last build |
|
|
263
|
+
| `ENCLOSURE_GENERATOR_STALE` | warning | Built by another version of the generator (`py/enclosure_cad.py` or `py/fit.py` changed): the recorded `sourceDigest` differs from the one recomputed for the job; release refuses it |
|
|
264
|
+
| `ENCLOSURE_FEATURE_MISSING` | error | An interface with no generated feature, or the reverse |
|
|
265
|
+
| `ENCLOSURE_FRAME_MISMATCH` | error | A declared frame differs from the generated one |
|
|
266
|
+
| `ENCLOSURE_GENERATOR` | error | A problem the generator reported (no room for a plunger flange, a feature that cut nothing, …) |
|
|
267
|
+
| `ENCLOSURE_COLLISION` | error | A body inside intersects a wall, boss or column (exact: any depth; voxel: past one cell); a plunger intersects a wall (exact) or a body inside (voxel, past one cell) |
|
|
268
|
+
| `ENCLOSURE_NOT_CONTAINED` | error | A body reaches outside the cavity (exact) |
|
|
269
|
+
| `ENCLOSURE_CLEARANCE` | warning | A body is closer to a wall than the design clearance (exact: by any amount; voxel: by more than one cell), or a plunger is closer than that to a body inside (voxel) |
|
|
270
|
+
| `ENCLOSURE_FIT_UNCHECKED` | warning | The contents changed and Python is not available to re-analyse the fit |
|
|
271
|
+
| `ENCLOSURE_THIN_WALL` | error / warning | Below the minimum wall: exact per feature (error), or a voxel estimate anywhere (warning) |
|
|
272
|
+
| `ENCLOSURE_MISALIGNED` | error | An opening, window, light pipe or plunger off the axis of what it serves |
|
|
273
|
+
| `ENCLOSURE_RECESS` | error | The served part sits deeper behind an opening than allowed |
|
|
274
|
+
| `ENCLOSURE_PLUNGER` | error | A plunger presses its switch at rest, cannot reach it, or its trimmed flange no longer holds it in |
|
|
275
|
+
| `ENCLOSURE_PLUG_FIT` | error / warning | The plug a socket opening takes does not pass it (error); its overmold has no flat seat (warning) |
|
|
276
|
+
| `ENCLOSURE_SEAL_SQUEEZE` | error | O-ring squeeze outside the range of Parker's chart row for its cross-section |
|
|
277
|
+
| `ENCLOSURE_SEAL_FIT` | error / warning | Stretch over 5 % (error); a face groove not sized to the O-ring (warning) |
|
|
278
|
+
| `ENCLOSURE_ENVELOPE` | error | Bigger than the module's `design_envelope` |
|
|
279
|
+
|
|
280
|
+
Core adds `protoboard/standard-outputs` (the files in `out/`) and, when the
|
|
281
|
+
module references the manifest, `uhd/geometryBindings`.
|
|
282
|
+
|
|
283
|
+
## In the Manufacturing Package
|
|
284
|
+
|
|
285
|
+
With @protoboardai/ext-release, `protoboard release` calls `ops/package.ts` for
|
|
286
|
+
each unit that builds an enclosure this provider designs:
|
|
287
|
+
|
|
288
|
+
- `package:work-order`: the print order of a `print` enclosure: material,
|
|
289
|
+
a printer profile (0.4 mm nozzle and 0.2 mm layers, stated as assumptions:
|
|
290
|
+
the minimum wall is two 0.4 mm extrusions; perimeters from `wall_mm`),
|
|
291
|
+
each printed part with its volume, orientation and supports (a side-wall
|
|
292
|
+
cut wider than 10 mm, an assumption for a tuned printer, needs them), and
|
|
293
|
+
post-processing: heat-set inserts, O-rings, plungers, lid screws.
|
|
294
|
+
- `package:test-plan`: the enclosure's own checks from the build report: seal
|
|
295
|
+
squeeze against the gland table, buttons, plugs, the lid joint, and every
|
|
296
|
+
body the fit analysis found tight.
|
|
297
|
+
|
|
298
|
+
`test/package.test.ts` checks both on a written build report.
|
|
299
|
+
|
|
300
|
+
## Limits
|
|
301
|
+
|
|
302
|
+
- Box and round shapes, one horizontal split plane, cuts square to a wall
|
|
303
|
+
(within 10°). No ribs, draft, chamfers or print-orientation analysis.
|
|
304
|
+
- Lid screws: four columns at the cavity's corners (box) or at 45° (round),
|
|
305
|
+
which bulge out of the outline; a round shell can close with a twist lid
|
|
306
|
+
instead (bayonet or thread), which stays inside it. Snap fits are not
|
|
307
|
+
generated. A twist lid's lug and thread sizes and clearances are for a
|
|
308
|
+
printed fit (defaults 0.2 mm radial, 0.15 mm axial), not from a standard:
|
|
309
|
+
check them on a first print. A bayonet's slots slide over the O-ring as the
|
|
310
|
+
lid goes on (grease it); there is no detent holding the turned lid.
|
|
311
|
+
- Threads: one start, a symmetric trapezoid approximating the semi-buttress
|
|
312
|
+
profile, no lead-in or run-out. Values for a real neck must come from a
|
|
313
|
+
measured or cited finish; the bottle project's open question OQ-B4 applies.
|
|
314
|
+
- Plungers need room for a flange between the wall and the switch; a switch
|
|
315
|
+
flush with the cavity wall is reported, not solved. A flange trimmed until
|
|
316
|
+
it no longer holds is reported; the generator does not add another
|
|
317
|
+
retainer. The flange is trimmed against sampled surfaces (0.1 mm apart,
|
|
318
|
+
with half that added to the clearance).
|
|
319
|
+
- The exact fit knows the cavity and the cylinders standing in it; anything
|
|
320
|
+
else inside the cavity (a hand-edited rib) is seen only by the voxel field,
|
|
321
|
+
within one cell.
|
|
322
|
+
- Only bosses declare contact zones; other intended contact (a cell taped to
|
|
323
|
+
the floor) needs the cavity clearance, or the body to be excluded.
|
|
324
|
+
- Fastener hardware is not part of the fit analysis.
|
|
325
|
+
- `extract` returns frames; writing them into the module source is a manual
|
|
326
|
+
re-binding step for now (no `enclosure-cad-rebind` operation yet).
|
|
327
|
+
- The fit analysis caps the grid at 6 million cells and coarsens the
|
|
328
|
+
resolution for large enclosures.
|
|
329
|
+
- lib3mf writes 3MF as ZIP64; core's structural 3MF check does not read
|
|
330
|
+
ZIP64, so the generator repacks each 3MF as a plain ZIP.
|
|
331
|
+
|
|
332
|
+
## How an Onshape provider would implement the same capability
|
|
333
|
+
|
|
334
|
+
The capability contract does not change; only the native design does.
|
|
335
|
+
|
|
336
|
+
- **Native design.** A Part Studio driven by a FeatureScript feature that
|
|
337
|
+
reads the same inputs this provider writes to `<id>.job.json`: contents as
|
|
338
|
+
imported meshes or STEP placed by the job's matrices, the features with
|
|
339
|
+
their served frames, the design values. The provider directory holds
|
|
340
|
+
`onshape/` with the document, workspace and element ids and the
|
|
341
|
+
FeatureScript source; those ids never go into UHD.
|
|
342
|
+
- **generate** creates or updates the document through the Onshape REST
|
|
343
|
+
API. That is an upload to a cloud service, so the operation would declare
|
|
344
|
+
an external effect and need `--authorize`; credentials come from the
|
|
345
|
+
environment, never from project files.
|
|
346
|
+
- **export** requests translations: STEP and GLB of the Part Studio, STL and
|
|
347
|
+
3MF per part (or DXF for flat parts with `process: "cut"`), into `out/`
|
|
348
|
+
under the same names.
|
|
349
|
+
- **body.manifest.json** comes from FeatureScript queries: each feature tags
|
|
350
|
+
its faces with an attribute holding the interface id, and an evaluated
|
|
351
|
+
function returns their area, centroid and normal.
|
|
352
|
+
- **extract** reads the same attributes for frames.
|
|
353
|
+
- **check** compares the document's microversion and the recorded job with
|
|
354
|
+
the model (drift), reports Onshape's own feature errors, and can reuse this
|
|
355
|
+
package's `py/fit.py` on the exported meshes for fit, walls and
|
|
356
|
+
containment, so both providers judge fit the same way.
|
|
357
|
+
|
|
358
|
+
Swapping is an edit to `cad.config.ts`; the module and everything
|
|
359
|
+
downstream of `out/` stay as they are.
|
|
360
|
+
|
|
361
|
+
## Tests
|
|
362
|
+
|
|
363
|
+
`test/enclosure-cad.test.ts` runs the provider contract on a synthetic
|
|
364
|
+
device (`test/fixture.ts`: a 30 × 24 mm board on four insert bosses, a cell,
|
|
365
|
+
a USB-C opening, two buttons): a two-part printed case whose fit checks
|
|
366
|
+
pass, with its STEP files probed exactly (`test/probe.py`): the plungers do
|
|
367
|
+
not touch the board or the cell, and no cut leaves a base or lid bridge
|
|
368
|
+
thinner than the minimum wall; a board 1 mm larger than the cavity is caught; a moved connector is
|
|
369
|
+
caught as misaligned; generation is byte-for-byte repeatable; a hand-edited
|
|
370
|
+
native design is rebuilt and reported; and the job that the other builders
|
|
371
|
+
(window, light pipe, lugs, thread, seal glands) produce through
|
|
372
|
+
`loadProject` is what the generator reads. `test/features.test.ts` builds a
|
|
373
|
+
round case (windows, a light pipe, lugs, a joint O-ring, screws), a bottle
|
|
374
|
+
cap (thread, lip and probe seals), a bayonet and a threaded twist lid on an
|
|
375
|
+
80 mm shell (the shell stays 80 mm across, the collar's levels, the seal's
|
|
376
|
+
stretch and squeeze through the check, and no volume shared by the built base
|
|
377
|
+
and lid), and a box case whose openings cross the lid joint, and runs the
|
|
378
|
+
check rules on a fabricated report. The generator tests need Python with build123d and scipy
|
|
379
|
+
(`PROTOBOARD_PYTHON`, else the quadcopter example's `tools install cad`
|
|
380
|
+
runtime, else the older `../uhd/.venv-cad`; see `packages/core/test/cad-python.ts`);
|
|
381
|
+
they are skipped, and say so,
|
|
382
|
+
otherwise.
|
|
383
|
+
|
|
384
|
+
## Evals
|
|
385
|
+
|
|
386
|
+
`evals/fit-grown-board`: the board in the synthetic device grows 2 mm and the
|
|
387
|
+
case no longer holds it; the agent makes the enclosure fit without touching
|
|
388
|
+
the board (needs Python with build123d). Run it with `protoboard evals
|
|
389
|
+
--extension enclosure-cad`; see the [evals guide](../../docs/guides/evals.md).
|