/* bench-plate.cjs — parametric bench plate for an ESP32-S3-DevKitC-1 + half-size breadboard, * and the fit gauge that makes the next revision exact. * * THE HONEST PROBLEM THIS DESIGN IS SHAPED AROUND * ----------------------------------------------- * CORRECTED 2026-08-08. This note used to say Espressif's board page is JS-rendered and its * linked dimensions file 404s. Both halves were wrong, and saying so here matters more than * the original note did. What actually happened is link rot: the unversioned user_guide.html * 404s, but the board's index page is plain server-rendered HTML and links the guide under a * versioned name, user_guide_v1.1.html, which serves 200 — and that guide does publish a * mechanical drawing, * * dl.espressif.com/dl/schematics/esp_idf/DXF_ESP32-S3-DevKitC-1_V1.1_20220429.dxf * 200, image/vnd.dxf, 743,753 bytes, re-checked 2026-08-08. * * So the vendor DOES ship an outline. What it does not ship is a NUMBER. That DXF is drawing * geometry carrying no annotation layer — an entity census of it, * * curl -sL | tr -d '\r' \ * | awk '{sub(/^ +/,"")} p=="0"{c[$0]++} {p=$0} END{for(k in c) print c[k], k}' \ * | sort -rn | head * * returns 2640 LWPOLYLINE, 226 SOLID, 113 TEXT and ZERO DIMENSION entities, and those TEXT * strings are reference designators (R12, U4, J2) plus title-block fields, not callouts. * Getting a width out of that file means loading the outline into CAD and measuring it, which * is a different job from reading a figure off a drawing, and it has not been done here. * dankbuild's hardest rule is that no hardware fact may be invented, so boardW/boardL below * are still declared UNVERIFIED, and the design is deliberately arranged so that being wrong * about them is CHEAP: * * - The board bay is OPEN AT BOTH ENDS. Get the length wrong and the board simply * overhangs; it does not fail to fit. Only the WIDTH is constrained, and it carries * 0.8 mm of clearance, so a full millimetre of error still assembles. * - The breadboard bay uses the standardised half-size (BB400) footprint, which is fixed * by 2.54 mm tie-point pitch rather than by any single vendor's tooling. * - esp32-fit-gauge.stl exists so the guess can be replaced by a MEASUREMENT for about * fifteen minutes of filament, after which this file regenerates an exact plate. * * All units mm. Z=0 is the build plate. Flat bottom, no overhang beyond 3.2 mm walls: * prints without supports, without a raft, and without a brim on any reasonable surface. */ 'use strict'; const { Model } = require('./stl.cjs'); /* ─── PARAMETERS ──────────────────────────────────────────────────────────────── Change these and re-run. Provenance is stated; anything unverified says so. */ const P = { // Half-size (BB400) solderless breadboard. Footprint follows from 2.54 mm pitch and is // consistent across vendors to a few tenths. Clearance is generous: moulded parts have draft. bbL: 83.5, bbW: 54.5, bbClear: 0.6, // ESP32-S3-DevKitC-1. UNVERIFIED — see the header. The bay is open-ended, so only width // is load-bearing, and width is exactly what the fit gauge measures. boardW: 25.4, boardL: 63.0, boardClear: 0.8, baseT: 2.4, // 6 layers at 0.4 mm — stiff enough not to cup wallT: 2.0, // 5 perimeters — no gap-fill artefacts wallH: 3.2, // retains parts without burying the pin rows margin: 2.0, // outer margin notchW: 12.0, // cable notch in the rear wall }; /* ─── THE BENCH PLATE ─────────────────────────────────────────────────────────── Seen from above (+Y is back): ┌──────────────────────────┬─────────────┐ │ │ │ rear wall carries two cable │ breadboard bay │ devkit bay │ notches so jumper wires leave │ │ │ the plate without being pinched └──────────────────────────┴─────────────┘ */ function benchPlate(p = P) { const bbBayW = p.bbL + p.bbClear; const bbBayD = p.bbW + p.bbClear; const dkBayW = p.boardW + p.boardClear; // PLATE DEPTH IS SET BY THE BREADBOARD, NOT THE BOARD. Sizing it to the longer of the two // (the first attempt) gave the breadboard a 63 mm bay for a 54.5 mm part — 8 mm of slop in // the one dimension I actually know. The devkit is instead handled by leaving the front and // rear walls OPEN across its bay, so a board longer than the plate simply overhangs at both // ends and is retained by width alone. That is what makes an unverified length harmless: // there is no wall for it to collide with. // NO SEPARATE `gap` TERM. It used to be added here, and because nothing ever occupied it, // all 4 mm fell into the devkit bay: the board got 4.8 mm of slop instead of 0.8 and would // have rattled. The divider IS the separation between the bays; there is nothing else to // budget for. Caught by test-fit.cjs measuring the finished solids rather than trusting // the arithmetic that produced them. const innerW = bbBayW + p.wallT + dkBayW; const W = innerW + 2 * p.wallT + 2 * p.margin; const D = bbBayD + 2 * p.wallT + 2 * p.margin; const m = new Model(); m.addAt(0, 0, 0, W, D, p.baseT); // base slab const z0 = p.baseT, z1 = p.baseT + p.wallH; const x0 = p.margin, x1 = W - p.margin; const y0 = p.margin, y1 = D - p.margin; const divX = x0 + p.wallT + bbBayW; // divider between the bays const dkOpen = [divX, x1]; // X span left open in the front and rear walls // Cable notches sit over the BREADBOARD half only — the devkit half is already open. const n1 = x0 + bbBayW * 0.22, n2 = x0 + bbBayW * 0.62; m.wallX(x0, x1, y0, z0, z1, p.wallT, [dkOpen]); m.wallX(x0, x1, y1 - p.wallT, z0, z1, p.wallT, [[n1, n1 + p.notchW], [n2, n2 + p.notchW], dkOpen]); // Side walls sit BETWEEN the end walls so no two boxes ever share volume; they meet // face-to-face at the corners, which is legal and prints solid. The right-hand wall must // run the FULL depth, because with the end walls open there is nothing else holding the // devkit bay closed on that side. m.wallY(y0 + p.wallT, y1 - p.wallT, x0, z0, z1, p.wallT); m.wallY(y0, y1, x1 - p.wallT, z0, z1, p.wallT); m.wallY(y0, y1, divX, z0, z1, p.wallT); return { m, meta: { bbBayW, bbBayD, dkBayW, W, D, overhang: p.boardL - D } }; } /* ─── THE FIT GAUGE ───────────────────────────────────────────────────────────── Six U-channels of increasing width. Lower the DevKitC-1 into each from above; the narrowest channel it settles into without forcing IS the board width, to within the 0.4 mm step. A rib count beside each channel identifies it (1 rib = narrowest), so the reading survives the part being picked up and put down. Channels are open at both ends and only 16 mm long, so a short section of the PCB edge is engaged — you measure the board outline rather than fighting the pin headers. RE-CENTRING THE LADDER. The ladder is only useful if it straddles the width you are hunting, and the published band straddles a DevKitC-1-sized board. A caller who is chasing a different board can pass a rough estimate as `centre` and get the same six channels re-centred on it: offsets of ±0.5, ±1.5 and ±2.5 steps, so three channels fall below the estimate and three above and none of them lands exactly on it. That invents no hardware fact — the estimate is the caller's, and the gauge's whole job is to disagree with it if it is wrong. WHY THE DEFAULT IS A LITERAL AND NOT A COMPUTED CENTRE. The published STL must not move. Writing the default band as the literal array it has always been, rather than as a computed centre-and-step, means no rounding path runs at all in the default case and the shipped bytes cannot drift. The channel COUNT is deliberately not a parameter: ribBandW below is sized for exactly six ribs, so a seventh channel would put its rib band under the neighbouring channel. */ function fitGauge(opts = {}) { const step = opts.step == null ? 0.4 : opts.step; // 2 dp keeps binary-float dust out of the mesh: a caller passing 31.7 would otherwise get // 31.7 - 1.5*0.4 = 31.099999999999998 as a channel width. const widths = opts.centre == null ? [24.6, 25.0, 25.4, 25.8, 26.2, 26.6] : [-2.5, -1.5, -0.5, 0.5, 1.5, 2.5].map((k) => Math.round((opts.centre + k * step) * 100) / 100); const chanLen = 16, wallT = 2.0, baseT = 2.4, wallH = 3.0; const ribW = 1.6, ribGap = 1.6, ribLen = 5.0, ribH = 1.2; const pitchPad = 6.0, margin = 3.0; const ribBandW = 6 * (ribW + ribGap) + 2; // room for up to six ribs const cellW = ribBandW + chanLen + margin; // TWO COLUMNS, NOT ONE. Stacking all six channels in a line made a 213 mm strip — it fits a // 220 mm bed only just, wastes most of the plate, and is an awkward thing to handle. A 2x3 // grid is the same six measurements in roughly a third of the footprint. const COLS = 2, ROWS = Math.ceil(widths.length / COLS); const rowPitch = Math.max(...widths) + 2 * wallT + pitchPad; const D = ROWS * rowPitch - pitchPad + 2 * margin; const W = COLS * cellW + margin; const m = new Model(); m.addAt(0, 0, 0, W, D, baseT); const z0 = baseT, z1 = baseT + wallH; const rows = widths.map((w, i) => { const col = i % COLS, row = Math.floor(i / COLS); const cx = margin + col * cellW; const y = margin + row * rowPitch; const chanX0 = cx + ribBandW; m.wallX(chanX0, chanX0 + chanLen, y, z0, z1, wallT); m.wallX(chanX0, chanX0 + chanLen, y + wallT + w, z0, z1, wallT); for (let k = 0; k <= i; k++) m.addAt(cx + k * (ribW + ribGap), y + wallT, z0, ribW, ribLen, ribH); return { w, y, cx, chanX0, chanLen, wallT, ribs: i + 1 }; }); return { m, meta: { widths, rows, W, D, baseT, wallH } }; } module.exports = { P, benchPlate, fitGauge };