@protoboardai/ext-enclosure-cad 0.0.0-stage → 0.2.1
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 +52 -4
- package/protoboard-extension.json +130 -0
- package/py/enclosure_cad.py +1731 -0
- package/py/fit.py +341 -0
- package/skills/enclosure-cad/SKILL.md +194 -0
package/ops/package.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contributions to the Manufacturing Package (@protoboardai/ext-release section
|
|
3
|
+
* contract) for the enclosures this provider designs: the print order of
|
|
4
|
+
* each printed enclosure in its unit's work order (material, printer
|
|
5
|
+
* profile, each part's orientation and supports, post-processing: supports
|
|
6
|
+
* off, inserts set, light pipes filled), and the enclosure's own checks in
|
|
7
|
+
* the test plan (seal squeeze, buttons, plugs, the lid joint, tight fits).
|
|
8
|
+
*
|
|
9
|
+
* The print order ends at the printed parts with their inserts set: the
|
|
10
|
+
* O-rings, plungers and lid screws go in during assembly, once what the
|
|
11
|
+
* enclosure holds is inside (the assembly guide's steps). When the enclosure
|
|
12
|
+
* is a unit of its own, built before the unit it is assembled into, its test
|
|
13
|
+
* plan keeps the checks a bare housing allows (a plug through its opening)
|
|
14
|
+
* and the unit it goes into gets the ones that need it assembled (seals,
|
|
15
|
+
* buttons, the lid joint, fits). Values
|
|
16
|
+
* come from the module's features, its cad.config.ts and the build report
|
|
17
|
+
* (build123d/<id>.report.json); the files are the standard outputs.
|
|
18
|
+
*/
|
|
19
|
+
import { join } from "node:path";
|
|
20
|
+
import type { CapabilityBinding, Project } from "@protoboardai/core";
|
|
21
|
+
import type { Block, SectionContext, SectionFn } from "@protoboardai/ext-release";
|
|
22
|
+
import { perBuild, providerModules } from "@protoboardai/ext-release/helpers";
|
|
23
|
+
import { nativePaths, readJson, type Report } from "../lib/check.ts";
|
|
24
|
+
import { glandRow } from "../lib/standards.ts";
|
|
25
|
+
|
|
26
|
+
const PROVIDER = "@protoboardai/ext-enclosure-cad";
|
|
27
|
+
/** Printer profile assumptions (stated in the order): the minimum print wall is two 0.4 mm extrusions (README). */
|
|
28
|
+
const NOZZLE_MM = 0.4;
|
|
29
|
+
const LAYER_MM = 0.2;
|
|
30
|
+
/** Widest side-wall opening printed without supports (a bridge); an assumption for a tuned FDM printer. */
|
|
31
|
+
const BRIDGE_MM = 10;
|
|
32
|
+
|
|
33
|
+
const n = (v: number, d = 1) => String(Number(v.toFixed(d)));
|
|
34
|
+
|
|
35
|
+
interface Enclosure {
|
|
36
|
+
b: CapabilityBinding;
|
|
37
|
+
name: string;
|
|
38
|
+
id: string;
|
|
39
|
+
report?: Report;
|
|
40
|
+
constraints: { wall_mm?: number; material?: string; fasteners?: string };
|
|
41
|
+
per: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function enclosures(p: Project, unit: SectionContext["unit"]): Enclosure[] {
|
|
45
|
+
return toEnclosures(p, unit, providerModules(p, unit, PROVIDER));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Enclosures built as units of their own and assembled into this unit (its sub-unit members). */
|
|
49
|
+
export function enclosuresAssembledIn(p: Project, unit: SectionContext["unit"]): Enclosure[] {
|
|
50
|
+
const subs = new Set(unit.members.filter((m) => m.unit).map((m) => m.definition));
|
|
51
|
+
return toEnclosures(p, unit, p.capabilities.filter((b) => !b.problem && b.module && b.provider === PROVIDER && subs.has(b.module)));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** The enclosure is this unit's own module, assembled into another unit (its parents). */
|
|
55
|
+
const ownUnit = (unit: SectionContext["unit"], e: Enclosure) => unit.kind !== "root" && unit.definition === e.id && unit.parents.length > 0;
|
|
56
|
+
|
|
57
|
+
function toEnclosures(p: Project, unit: SectionContext["unit"], bs: CapabilityBinding[]): Enclosure[] {
|
|
58
|
+
return bs.map((b) => {
|
|
59
|
+
const def = p.definitions.get(b.module!)!.def;
|
|
60
|
+
const report = b.native ? readJson<Report>(join(p.root, nativePaths(b.native, def.id).report)) : undefined;
|
|
61
|
+
return { b, name: def.name ?? def.id, id: def.id, report, constraints: b.constraints as Enclosure["constraints"], per: perBuild(unit, def.id) };
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sideways = (f: { frame?: { normal: number[] } }) => !!f.frame && Math.abs(f.frame.normal[2]) < 0.7;
|
|
66
|
+
|
|
67
|
+
function orientation(part: string, e?: Enclosure): string {
|
|
68
|
+
// a twist lid states how its base and lid print without supports under their lock faces
|
|
69
|
+
const twist = Object.values(e?.report?.features ?? {}).find((f) => f.kind === "twist_lid");
|
|
70
|
+
const said = (twist?.print as Record<string, string> | undefined)?.[part];
|
|
71
|
+
if (said) return said;
|
|
72
|
+
if (part === "base") return "floor on the bed, open side up";
|
|
73
|
+
if (part === "lid") return "outer face on the bed";
|
|
74
|
+
if (part.startsWith("plunger")) return "flange on the bed";
|
|
75
|
+
return "largest flat face on the bed";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function supports(e: Enclosure, part: string): string {
|
|
79
|
+
const feats = Object.entries(e.report?.features ?? {}).filter(([, f]) => f.parts.includes(part) && sideways(f) && ["opening", "button", "light_pipe", "window"].includes(f.kind));
|
|
80
|
+
const wide = feats.filter(([, f]) => {
|
|
81
|
+
const size = (f.size_mm ?? f.aperture_mm) as number[] | undefined;
|
|
82
|
+
const d = (f.hole_d_mm as number | undefined) ?? (size ? Math.max(size[0], size[1]) : 0);
|
|
83
|
+
return d > BRIDGE_MM;
|
|
84
|
+
});
|
|
85
|
+
if (wide.length) return `under ${wide.map(([id]) => id).join(", ")} (wider than ${BRIDGE_MM} mm)`;
|
|
86
|
+
return feats.length ? `none: ${feats.length} side-wall cut${feats.length === 1 ? "" : "s"} bridge ≤ ${BRIDGE_MM} mm` : "none";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export const workOrder: SectionFn = ({ project: p, unit, release }: SectionContext) => {
|
|
90
|
+
const es = enclosures(p, unit).filter((e) => e.b.process === "print");
|
|
91
|
+
if (!es.length) return undefined;
|
|
92
|
+
const builds = unit.perProduct * release.build;
|
|
93
|
+
const blocks: Block[] = [];
|
|
94
|
+
for (const e of es) {
|
|
95
|
+
const wall = e.constraints.wall_mm;
|
|
96
|
+
const perimeters = wall ? Math.max(2, Math.round(wall / (NOZZLE_MM + 0.05))) : undefined;
|
|
97
|
+
const parts = Object.entries(e.report?.parts ?? {});
|
|
98
|
+
blocks.push({ kind: "heading", text: e.name });
|
|
99
|
+
blocks.push({
|
|
100
|
+
kind: "keyvalues",
|
|
101
|
+
rows: [
|
|
102
|
+
{ label: "Process", value: "FDM printing", note: `${e.b.config}: process print` },
|
|
103
|
+
{ label: "Material", value: e.constraints.material ?? "not stated: state it on the order (cad.config.ts material)" },
|
|
104
|
+
{ label: "Profile", value: `${NOZZLE_MM} mm nozzle, ${LAYER_MM} mm layers${perimeters ? `, ${perimeters} perimeters (walls ${wall} mm)` : ""}, solid infill`, note: "assumption: the design's 0.8 mm minimum wall is two 0.4 mm extrusions; walls this thin print as perimeters only" },
|
|
105
|
+
{ label: "Quantity", value: `${e.per * builds} set${e.per * builds === 1 ? "" : "s"} of ${parts.length || "?"} part${parts.length === 1 ? "" : "s"}`, note: `${e.per} per build × ${builds}` },
|
|
106
|
+
...(e.report?.overall ? [{ label: "Overall", value: [0, 1, 2].map((k) => n(e.report!.overall.max[k] - e.report!.overall.min[k])).join(" × ") + " mm" }] : []),
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
if (parts.length)
|
|
110
|
+
blocks.push({
|
|
111
|
+
kind: "table",
|
|
112
|
+
columns: [{ key: "part", label: "Part" }, { key: "vol", label: "Volume", align: "right" }, { key: "orient", label: "Orientation" }, { key: "sup", label: "Supports" }, { key: "file", label: "File" }],
|
|
113
|
+
rows: parts.map(([k, v]) => ({ part: k, vol: `${n(v.volume_mm3 / 1000, 2)} cm³`, orient: orientation(k, e), sup: supports(e, k), file: `files/mechanical/${e.id}/${k}.3mf` })),
|
|
114
|
+
caption: `${e.name}: parts`,
|
|
115
|
+
});
|
|
116
|
+
const def = p.definitions.get(e.id)!.def;
|
|
117
|
+
const post: string[] = ["Remove brim and any supports; deburr every opening and bore."];
|
|
118
|
+
let inserts = 0;
|
|
119
|
+
let insertName = "";
|
|
120
|
+
for (const f of Object.values(e.report?.features ?? {})) {
|
|
121
|
+
const ins = f.insert as { name?: string } | undefined;
|
|
122
|
+
if (f.kind === "boss") {
|
|
123
|
+
inserts += (f.holes as unknown[] | undefined)?.length ?? 0;
|
|
124
|
+
insertName ||= ins?.name ?? "";
|
|
125
|
+
}
|
|
126
|
+
if (f.kind === "joint" && Array.isArray(f.columns)) {
|
|
127
|
+
inserts += f.columns.length;
|
|
128
|
+
insertName ||= ins?.name ?? "";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (inserts) post.push(`Set ${inserts} heat-set insert${inserts === 1 ? "" : "s"}${insertName ? ` (${insertName})` : ""} with a soldering iron and insert tip at the material's insert temperature; each flush and square.`);
|
|
132
|
+
// light pipes the module says are filled after printing (LightPipe fill)
|
|
133
|
+
const fills = new Map<string, string[]>();
|
|
134
|
+
for (const [id, f] of Object.entries(e.report?.features ?? {}).filter(([, f]) => f.kind === "light_pipe")) {
|
|
135
|
+
const fill = featureParams(def, id)?.fill;
|
|
136
|
+
if (typeof fill === "string") fills.set(fill, [...(fills.get(fill) ?? []), id]);
|
|
137
|
+
}
|
|
138
|
+
for (const [fill, ids] of fills) post.push(`Fill the ${ids.length === 1 ? `${ids[0]} light pipe` : `${ids.length} light pipes (${ids.join(", ")})`} with ${fill}, flush with the outer face; cure fully before handling.`);
|
|
139
|
+
// fitted during assembly, once the contents are in: named here so the print order does not do them
|
|
140
|
+
const later: string[] = [];
|
|
141
|
+
const glands = Object.values(e.report?.features ?? {}).filter((f) => f.kind === "seal_gland").map((f) => String(f.oring ?? "O-ring"));
|
|
142
|
+
if (glands.length) later.push(`the O-ring${glands.length === 1 ? "" : "s"} (${glands.join(", ")})`);
|
|
143
|
+
const plungers = Object.values(e.report?.features ?? {}).filter((f) => f.kind === "button").length;
|
|
144
|
+
if (plungers) later.push(plungers === 1 ? "the button plunger" : `the ${plungers} button plungers`);
|
|
145
|
+
const joint = Object.values(e.report?.features ?? {}).find((f) => f.kind === "joint" && f.screw);
|
|
146
|
+
if (joint) later.push(`the lid and its ${Array.isArray(joint.columns) ? `${joint.columns.length} × ` : ""}${(joint.screw as { name?: string }).name ?? "screws"}`);
|
|
147
|
+
const twist = Object.values(e.report?.features ?? {}).find((f) => f.kind === "twist_lid");
|
|
148
|
+
if (twist) later.push(`the lid (${twist.lock === "bayonet" ? `a bayonet: ${twist.lugs} lugs, ${twist.twist_deg}° turn` : `threaded: ${twist.turns} turns at ${twist.pitch_mm} mm`})`);
|
|
149
|
+
if (later.length) {
|
|
150
|
+
const where = ownUnit(unit, e) ? `at assembly in ${unit.parents.map((x) => (x === "system" ? "final assembly (system/)" : `unit ${x} (units/${x}/)`)).join(", ")}` : "during assembly, once what the enclosure holds is inside (the assembly operations)";
|
|
151
|
+
post.push(`Keep the parts open and unassembled: ${later.join(", ")} ${later.length === 1 && !/s$/.test(later[0]) ? "is" : "are"} fitted ${where}.`);
|
|
152
|
+
}
|
|
153
|
+
blocks.push({ kind: "list", items: post });
|
|
154
|
+
}
|
|
155
|
+
return { title: "Printed parts", order: 30, blocks };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const testPlan: SectionFn = ({ project: p, unit }: SectionContext) => {
|
|
159
|
+
// this unit's enclosures, and those built as sub-units and assembled here (their assembled checks are made here)
|
|
160
|
+
const own = enclosures(p, unit).filter((e) => e.report);
|
|
161
|
+
const es = [...own, ...enclosuresAssembledIn(p, unit).filter((e) => e.report)];
|
|
162
|
+
if (!es.length) return undefined;
|
|
163
|
+
const rows: { check: string; how: string; pass: string }[] = [];
|
|
164
|
+
let bare = false;
|
|
165
|
+
for (const e of es) {
|
|
166
|
+
// a housing built as its own unit: only what a bare housing shows here; the rest where it is assembled
|
|
167
|
+
const housingOnly = own.includes(e) && ownUnit(unit, e);
|
|
168
|
+
const assembledOnly = !own.includes(e);
|
|
169
|
+
bare ||= housingOnly;
|
|
170
|
+
for (const [id, f] of Object.entries(e.report!.features)) {
|
|
171
|
+
const needsAssembly = f.kind !== "opening";
|
|
172
|
+
if ((housingOnly && needsAssembly) || (assembledOnly && !needsAssembly)) continue;
|
|
173
|
+
if (f.kind === "seal_gland") {
|
|
174
|
+
const w = f.w_mm as number;
|
|
175
|
+
const depth = (f.gland_depth_mm as number) + ((f.gap_mm as number) ?? 0);
|
|
176
|
+
const row = glandRow(f.type as "face" | "radial", w);
|
|
177
|
+
rows.push({ check: `Seal ${id}: ${e.name}`, how: "Close the joint on the O-ring; inspect the squeeze line through the gap or by a witness mark", pass: `O-ring seated all round, not pinched; design squeeze ${n(((w - depth) / w) * 100)} %${row ? ` (${row.squeeze_pct[0]}–${row.squeeze_pct[1]} %, ${row.source})` : ""}` });
|
|
178
|
+
}
|
|
179
|
+
if (f.kind === "button") rows.push({ check: `Button ${id}: ${e.name}`, how: "Press the plunger; release", pass: `the switch actuates within the plunger's travel and the plunger returns${f.gap_mm !== undefined ? ` (design gap ${f.gap_mm} mm at rest)` : ""}` });
|
|
180
|
+
if (f.kind === "opening" && f.plug) rows.push({ check: `Opening ${id}: ${e.name}`, how: `Insert ${(f.plug as { name?: string }).name ?? "the mating plug"}`, pass: "it goes fully home and the overmold seats against the wall" });
|
|
181
|
+
if (f.kind === "joint") rows.push({ check: `Lid joint: ${e.name}`, how: "Close the lid and fit its screws", pass: "the lid closes flush all round; screws seat without bottoming in the inserts" });
|
|
182
|
+
if (f.kind === "twist_lid")
|
|
183
|
+
rows.push(
|
|
184
|
+
f.lock === "bayonet"
|
|
185
|
+
? { check: `Lid joint: ${e.name}`, how: `Push the lid down with its lugs in the slots and turn it ${f.twist_deg}°; try to lift it; turn it back`, pass: `it seats on the rim and turns to the slots' ends by hand, features in the lid line up with what they serve, it cannot be lifted when turned, and it opens again; skirt bore Ø${f.bore_d_mm} mm` }
|
|
186
|
+
: { check: `Lid joint: ${e.name}`, how: `Screw the lid down by hand (${f.turns} turns); unscrew it`, pass: `it runs without binding and seats on the rim; it comes off again; thread ${f.major_d_mm} mm × ${f.pitch_mm} mm` },
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
if (housingOnly) continue;
|
|
190
|
+
for (const b of e.report!.fit.bodies.filter((x) => x.status === "tight" || x.status === "collision"))
|
|
191
|
+
rows.push({ check: `Fit of ${b.label}: ${e.name}`, how: "Assemble and look for contact", pass: `no contact${b.near ? ` near ${b.near}` : ""} (the design leaves ${b.min_clearance_mm !== undefined ? `${n(b.min_clearance_mm, 2)} mm` : "little clearance"} here)` });
|
|
192
|
+
}
|
|
193
|
+
if (!rows.length) return undefined;
|
|
194
|
+
return {
|
|
195
|
+
title: "Enclosure",
|
|
196
|
+
order: 30,
|
|
197
|
+
blocks: [
|
|
198
|
+
{
|
|
199
|
+
kind: "paragraph",
|
|
200
|
+
text: bare
|
|
201
|
+
? `With the first-article inspection, on the bare housing; the seals, buttons, the lid joint and the fits are checked where it is assembled (${unit.parents.map((x) => (x === "system" ? "system/" : `units/${x}/`)).join(", ")}).`
|
|
202
|
+
: "With the first-article inspection and again after final assembly: what the enclosure's design depends on.",
|
|
203
|
+
keep: true,
|
|
204
|
+
},
|
|
205
|
+
{ kind: "table", columns: [{ key: "check", label: "Check" }, { key: "how", label: "How" }, { key: "pass", label: "Pass" }], rows },
|
|
206
|
+
],
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
/** The enclosure_feature parameters of one of the module's feature interfaces. */
|
|
211
|
+
function featureParams(def: { interfaces: { id: string; traits?: { type: string; params?: Record<string, unknown> }[] }[] }, id: string): Record<string, unknown> | undefined {
|
|
212
|
+
return def.interfaces.find((i) => i.id === id)?.traits?.find((t) => t.type === "enclosure_feature")?.params;
|
|
213
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,54 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protoboardai/ext-enclosure-cad",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Protoboard extension: printed or machined enclosures generated from your design.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Delta Robotics, Inc.",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/Delta-Robotics-Inc/protoboard-stack.git",
|
|
10
|
+
"directory": "packages/ext-enclosure-cad"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"files": [
|
|
14
|
+
"protoboard-extension.json",
|
|
15
|
+
"README.md",
|
|
16
|
+
"py",
|
|
17
|
+
"lib",
|
|
18
|
+
"ops",
|
|
19
|
+
"skills",
|
|
20
|
+
"evals",
|
|
21
|
+
"LICENSE",
|
|
22
|
+
"NOTICE"
|
|
23
|
+
],
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@protoboardai/ext-harness-cad": "^0.2.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"@protoboardai/core": "^0.2.0",
|
|
35
|
+
"@deltarobotics/uhd": ">=0.3.0",
|
|
36
|
+
"@protoboardai/ext-release": "^0.2.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependenciesMeta": {
|
|
39
|
+
"@protoboardai/ext-release": {
|
|
40
|
+
"optional": true
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"protoboard",
|
|
45
|
+
"hardware",
|
|
46
|
+
"cad",
|
|
47
|
+
"enclosure",
|
|
48
|
+
"build123d"
|
|
49
|
+
],
|
|
50
|
+
"homepage": "https://protoboard.ai",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/Delta-Robotics-Inc/protoboard-stack/issues"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "protoboard.extension/v0",
|
|
3
|
+
"id": "@protoboardai/ext-enclosure-cad",
|
|
4
|
+
"version": "0.2.1",
|
|
5
|
+
"description": "Enclosure CAD: a printed or machined case generated with build123d from the UHD features it must provide (openings, plungers, windows, light pipes, insert bosses, O-ring glands, threads, strap lugs, the lid joint) and the bodies the assembly places inside it; fit, wall, seal and alignment checks.",
|
|
6
|
+
"compatibility": {
|
|
7
|
+
"protoboard": "^0.2.0",
|
|
8
|
+
"uhd": ">=0.3.0"
|
|
9
|
+
},
|
|
10
|
+
"skills": [
|
|
11
|
+
{
|
|
12
|
+
"id": "enclosure-cad",
|
|
13
|
+
"summary": "Describe an enclosure as a UHD module of features, configure the cad provider, generate, export and read the fit, wall, seal and alignment findings",
|
|
14
|
+
"path": "skills/enclosure-cad/SKILL.md"
|
|
15
|
+
}
|
|
16
|
+
],
|
|
17
|
+
"tools": [
|
|
18
|
+
{
|
|
19
|
+
"id": "python-build123d",
|
|
20
|
+
"purpose": "Python with build123d, numpy and scipy (enclosure generator; the fit analysis uses ext-harness-cad's voxel clearance field)",
|
|
21
|
+
"probe": [
|
|
22
|
+
"sh",
|
|
23
|
+
"-c",
|
|
24
|
+
"exec \"${PROTOBOARD_PYTHON:-python3}\" -c 'import build123d, numpy, scipy; print(\"build123d \" + build123d.__version__)'"
|
|
25
|
+
],
|
|
26
|
+
"install": "protoboard tools install cad (a project venv with these pins), or point PROTOBOARD_PYTHON at an interpreter with build123d==0.13.0 numpy==2.5.3 scipy==1.18.1",
|
|
27
|
+
"runtime": {
|
|
28
|
+
"id": "cad",
|
|
29
|
+
"kind": "python",
|
|
30
|
+
"packages": [
|
|
31
|
+
"build123d==0.13.0",
|
|
32
|
+
"numpy==2.5.3",
|
|
33
|
+
"scipy==1.18.1"
|
|
34
|
+
],
|
|
35
|
+
"env": "PROTOBOARD_PYTHON",
|
|
36
|
+
"python": ">=3.11,<3.15"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"capabilities": [
|
|
41
|
+
{
|
|
42
|
+
"id": "cad",
|
|
43
|
+
"summary": "Enclosures (cases, caps, back covers) with build123d: generated from the features they provide and what goes inside them",
|
|
44
|
+
"native": "build123d",
|
|
45
|
+
"operations": {
|
|
46
|
+
"generate": {
|
|
47
|
+
"entry": "./ops/cad.ts#generate",
|
|
48
|
+
"requires": [
|
|
49
|
+
"python-build123d"
|
|
50
|
+
],
|
|
51
|
+
"summary": "Write the build123d design (job + script) and build it: parts, body, report with the fit analysis"
|
|
52
|
+
},
|
|
53
|
+
"export": {
|
|
54
|
+
"entry": "./ops/cad.ts#exportOutputs",
|
|
55
|
+
"requires": [
|
|
56
|
+
"python-build123d"
|
|
57
|
+
],
|
|
58
|
+
"summary": "body.step, body.glb, body.manifest.json and, for print, an STL and a 3MF per printed part"
|
|
59
|
+
},
|
|
60
|
+
"extract": {
|
|
61
|
+
"entry": "./ops/cad.ts#extract",
|
|
62
|
+
"summary": "Generated feature frames by interface id, and the artifacts the module should reference"
|
|
63
|
+
},
|
|
64
|
+
"check": {
|
|
65
|
+
"entry": "./ops/cad.ts#check",
|
|
66
|
+
"summary": "Drift against the model and the assembly, fit (collisions, clearance, containment), walls, seal squeeze, opening and plunger alignment, envelope"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"constraints": {
|
|
70
|
+
"wall_mm": {
|
|
71
|
+
"description": "side wall thickness",
|
|
72
|
+
"required": true
|
|
73
|
+
},
|
|
74
|
+
"floor_mm": {
|
|
75
|
+
"description": "floor thickness (default: wall_mm)"
|
|
76
|
+
},
|
|
77
|
+
"lid_mm": {
|
|
78
|
+
"description": "lid thickness (default: wall_mm)"
|
|
79
|
+
},
|
|
80
|
+
"clearance_mm": {
|
|
81
|
+
"description": "gap between what goes inside and the walls (default 0.3)"
|
|
82
|
+
},
|
|
83
|
+
"clearance_top_mm": {
|
|
84
|
+
"description": "gap above the tallest part inside (default: clearance_mm)"
|
|
85
|
+
},
|
|
86
|
+
"min_wall_mm": {
|
|
87
|
+
"description": "thinnest wall allowed anywhere (default by process: print 0.8, machine 0.5)"
|
|
88
|
+
},
|
|
89
|
+
"material": {
|
|
90
|
+
"description": "material, e.g. PETG (recorded in the manifest; mass = volume × density)"
|
|
91
|
+
},
|
|
92
|
+
"shape": {
|
|
93
|
+
"description": "box (rounded rectangle) or round"
|
|
94
|
+
},
|
|
95
|
+
"cavity_mm": {
|
|
96
|
+
"description": "fixed cavity instead of one sized from the contents: { min, max } (box) or { centre, radius, z } (round)"
|
|
97
|
+
},
|
|
98
|
+
"split_z_mm": {
|
|
99
|
+
"description": "split plane height between base and lid (default: under the lid, at the joint)"
|
|
100
|
+
},
|
|
101
|
+
"joint": {
|
|
102
|
+
"description": "lid joint: { style: rabbet | flat, lip_height_mm, clearance_mm } (a joint O-ring makes it flat)"
|
|
103
|
+
},
|
|
104
|
+
"fasteners": {
|
|
105
|
+
"description": "screws (into heat-set inserts, from the joint interface) or none"
|
|
106
|
+
},
|
|
107
|
+
"exclude": {
|
|
108
|
+
"description": "instance paths placed in the assembly that are not inside the enclosure"
|
|
109
|
+
},
|
|
110
|
+
"resolution_mm": {
|
|
111
|
+
"description": "voxel size of the fit analysis (default 0.25)"
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
"operations": [
|
|
117
|
+
{
|
|
118
|
+
"id": "package:work-order",
|
|
119
|
+
"summary": "Manufacturing Package: the print order of each printed enclosure (material, profile, each part's orientation and supports, inserts, O-rings, plungers, lid screws)",
|
|
120
|
+
"entry": "./ops/package.ts#workOrder",
|
|
121
|
+
"effect": "pure"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
"id": "package:test-plan",
|
|
125
|
+
"summary": "Manufacturing Package: the enclosure's checks (seal squeeze, buttons, plugs, lid joint, tight fits) from its build report",
|
|
126
|
+
"entry": "./ops/package.ts#testPlan",
|
|
127
|
+
"effect": "pure"
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
}
|