@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 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
- # Temporary Holding Version
1
+ # @protoboardai/ext-enclosure-cad
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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).