#!/usr/bin/env node /* test-fit.cjs — assert the dimensions that decide whether the parts actually WORK. * * The mesh checks prove the file is valid geometry; the renders let a human sanity-check the * shape. Neither says the breadboard fits. These do, by measuring the finished solids the * same way a caliper would: take the boxes, and for a given Z height, ask what clear span is * left between them along a scan line. If a wall moved, or a clearance got typo'd, or a bay * silently inherited the wrong dimension, this fails — and it fails BEFORE filament is spent. */ 'use strict'; const assert = require('assert'); const { benchPlate, fitGauge, jumperComb, P } = require('./models.cjs'); let pass = 0; const ok = (name, fn) => { try { fn(); console.log(` ok ${name}`); pass++; } catch (e) { console.log(` FAIL ${name}\n ${e.message}`); process.exitCode = 1; } }; /* Largest clear gap along one axis between the solids that block a scan line, within the window [from,to]. Returns the gap and its bounds — what a part dropped in there can occupy. * * CLAMP THE BLOCKERS TO THE WINDOW. The first version did not, and it silently measured * geometry 70 mm outside the range it was asked about: every gauge channel came back as * 26.20 mm regardless of its real width, because the widest gap ANYWHERE on that scan line * won. A measuring instrument that ignores its own bounds reports confident nonsense, and it * would have failed a correct model — the exact way a bad test costs more than no test. */ function span(blocked, from, to) { const segs = blocked .map(([s, e]) => [Math.max(s, from), Math.min(e, to)]) .filter(([s, e]) => e > s) .sort((a, b) => a[0] - b[0]); let best = 0, at = null, cursor = from; for (const [s, e] of segs) { if (s > cursor && s - cursor > best) { best = s - cursor; at = [cursor, s]; } cursor = Math.max(cursor, e); } if (to > cursor && to - cursor > best) { best = to - cursor; at = [cursor, to]; } return { width: best, at }; } const clearSpanX = (boxes, y, z, from, to) => span(boxes .filter((b) => y > b[1] + 1e-9 && y < b[4] - 1e-9 && z > b[2] + 1e-9 && z < b[5] - 1e-9) .map((b) => [b[0], b[3]]), from, to); const clearSpanY = (boxes, x, z, from, to) => span(boxes .filter((b) => x > b[0] + 1e-9 && x < b[3] - 1e-9 && z > b[2] + 1e-9 && z < b[5] - 1e-9) .map((b) => [b[1], b[4]]), from, to); console.log('\nbench plate'); { const { m, meta } = benchPlate(); const boxes = m.boxes; const zMid = P.baseT + P.wallH / 2; // halfway up the walls const { W, D } = meta; ok('the breadboard bay is wide enough for a BB400, and not sloppy', () => { const span = clearSpanX(boxes, D / 2, zMid, 0, W); // the widest clear span across the middle is the breadboard bay assert(span.width >= P.bbL, `bay ${span.width.toFixed(2)} < breadboard ${P.bbL}`); assert(span.width <= P.bbL + P.bbClear + 0.01, `bay ${span.width.toFixed(2)} is sloppier than ${P.bbL + P.bbClear}`); }); ok('the breadboard bay is DEEP enough, and not sloppy', () => { const span = clearSpanY(boxes, P.margin + P.wallT + 5, zMid, 0, D); assert(span.width >= P.bbW, `depth ${span.width.toFixed(2)} < breadboard ${P.bbW}`); assert(span.width <= P.bbW + P.bbClear + 0.01, `depth ${span.width.toFixed(2)} is sloppier than ${P.bbW + P.bbClear}`); }); ok('the devkit bay holds the board width with the intended clearance', () => { const span = clearSpanX(boxes, D / 2, zMid, W - P.margin - P.wallT - 40, W); assert(Math.abs(span.width - (P.boardW + P.boardClear)) < 0.01, `devkit bay ${span.width.toFixed(2)}, expected ${(P.boardW + P.boardClear).toFixed(2)}`); }); ok('the devkit bay is OPEN end to end, so an unverified board LENGTH cannot bind', () => { // scan down the middle of the devkit bay: nothing may block it anywhere in Y const xMid = W - P.margin - P.wallT - (P.boardW + P.boardClear) / 2; const span = clearSpanY(boxes, xMid, zMid, 0, D); assert(Math.abs(span.width - D) < 0.01, `devkit bay is obstructed: clear span ${span.width.toFixed(2)} of ${D.toFixed(2)} mm`); }); ok('the rear wall really is notched for cables', () => { const yRear = D - P.margin - P.wallT / 2; const blocked = boxes.filter((b) => yRear > b[1] && yRear < b[4] && zMid > b[2] && zMid < b[5]); assert(blocked.length >= 3, `rear wall is ${blocked.length} segment(s); notches would make 3+`); }); ok('nothing overhangs: every solid sits on the base or on another solid', () => { for (const b of boxes) { if (b[2] < 1e-9) continue; // on the plate assert(Math.abs(b[2] - P.baseT) < 1e-9, `a solid starts at z=${b[2]}, not on the base`); } }); } console.log('\nfit gauge'); { const { m, meta } = fitGauge(); const boxes = m.boxes; const z = 2.4 + 3.0 / 2; ok('every channel measures its stated width', () => { // walk each channel by finding the pair of walls that bracket it for (const w of meta.widths) { const found = meta.rows.find((r) => Math.abs(r.w - w) < 1e-9); const span = clearSpanY(boxes, found.chanX0 + 1, z, found.y - 1, found.y + 2 * 2.0 + w + 1); assert(Math.abs(span.width - w) < 0.01, `channel ${w}: clear span ${span.width.toFixed(2)}`); } }); ok('the ribs never intrude into a channel', () => { for (const r of meta.rows) { for (const b of boxes) { const isRib = (b[3] - b[0]) < 2.0 && b[2] > 2.0; if (!isRib) continue; const inChannelY = b[1] < r.y + 2.0 + r.w && b[4] > r.y + 2.0; const inChannelX = b[0] < r.chanX0 + 16 && b[3] > r.chanX0; assert(!(inChannelY && inChannelX), `a rib overlaps channel ${r.w}`); } } }); ok('channel widths bracket the nominal 25.4 mm guess in both directions', () => { assert(Math.min(...meta.widths) < 25.4, 'no channel narrower than the guess'); assert(Math.max(...meta.widths) > 25.4, 'no channel wider than the guess'); }); ok('the PUBLISHED band is exactly the array the STL on the site was built from', () => { // The default must be immovable: html/electronics/esp32-bench-parts/esp32-fit-gauge.stl is // served from this array, so a default that drifts makes the published solid disagree // with the generator and with the dimensions both pages state. assert.deepStrictEqual(meta.widths, [24.6, 25.0, 25.4, 25.8, 26.2, 26.6]); }); } /* A RE-CENTRED GAUGE. The default band brackets a DevKitC-1-sized board and nothing else, so the method is only reusable if the ladder can be moved onto a width the reader supplies. These assertions are the ones that decide whether that variant is usable at all: does it still straddle the target in BOTH directions, is it still six channels, and is the solid still a printable mesh? A variant that brackets nothing is a ruler with no marks on it. */ console.log('\nfit gauge, re-centred on a supplied width'); { const target = 31.7; // an arbitrary caller estimate, not a board fact const { m, meta } = fitGauge({ centre: target }); const boxes = m.boxes; ok('a supplied centre is bracketed in both directions', () => { assert(meta.widths.length === 6, `${meta.widths.length} channels, not six`); assert(Math.min(...meta.widths) < target, `no channel narrower than ${target}`); assert(Math.max(...meta.widths) > target, `no channel wider than ${target}`); // Nothing lands ON the estimate: half-step offsets mean every channel gives a verdict. for (const w of meta.widths) assert(Math.abs(w - target) > 1e-9, `channel ${w} sits on the estimate`); }); ok('the re-centred ladder keeps the 0.4 mm default step, evenly spaced', () => { const sorted = meta.widths.slice().sort((a, b) => a - b); for (let i = 1; i < sorted.length; i++) { assert(Math.abs(sorted[i] - sorted[i - 1] - 0.4) < 1e-9, `step ${sorted[i - 1]} -> ${sorted[i]} is not 0.4`); } // 2 dp, so no 31.099999999999998 reaches the mesh. for (const w of meta.widths) assert(Math.abs(w * 100 - Math.round(w * 100)) < 1e-9, `${w} is not 2 dp`); }); ok('every re-centred channel measures its stated width, and no rib intrudes', () => { const z = meta.baseT + meta.wallH / 2; for (const r of meta.rows) { const s = clearSpanY(boxes, r.chanX0 + 1, z, r.y - 1, r.y + 2 * r.wallT + r.w + 1); assert(Math.abs(s.width - r.w) < 0.01, `channel ${r.w}: clear span ${s.width.toFixed(2)}`); for (const b of boxes) { const isRib = (b[3] - b[0]) < 2.0 && b[2] > 2.0; if (!isRib) continue; const inY = b[1] < r.y + r.wallT + r.w && b[4] > r.y + r.wallT; const inX = b[0] < r.chanX0 + r.chanLen && b[3] > r.chanX0; assert(!(inY && inX), `a rib overlaps re-centred channel ${r.w}`); } } }); ok('the re-centred solid is still a valid, non-interpenetrating mesh', () => { const v = m.check(); assert(v.ok, `mesh invalid: ${(v.problems || []).join('; ')}`); assert(v.triangles > 0 && v.solids === 34, `${v.solids} solids, expected the same 34 as the default`); }); ok('a re-centred gauge does not disturb the default one', () => { // Same process, called again after the variant: the published band must be unchanged. assert.deepStrictEqual(fitGauge().meta.widths, [24.6, 25.0, 25.4, 25.8, 26.2, 26.6]); }); } /* THE JUMPER-WIRE COMB. The only thing this part promises is a SPACING, so the only assertions worth making are about spacing — measured off the finished solids with the same scan line the plate and the gauge get, never read back out of the meta that produced them. If the teeth drifted off pitch the comb would still print, still look right in a render, and still be useless, which is exactly the failure the scan line exists to catch. */ console.log('\njumper-wire comb'); { const { m, meta } = jumperComb(); const boxes = m.boxes; const z = P.baseT + P.wallH / 2; // halfway up the teeth const yMid = P.margin + meta.toothD / 2; // down the middle of the gripping length ok('the teeth stand on exact 2.54 mm tie-point centres', () => { // Left face of every tooth, taken from the solids, not from the loop that placed them. const lefts = boxes .filter((b) => b[2] > P.baseT - 1e-9 && b[5] > b[2]) .map((b) => b[0]) .sort((a, b) => a - b); assert(lefts.length === meta.teeth, `${lefts.length} teeth, expected ${meta.teeth}`); for (let i = 1; i < lefts.length; i++) { assert(Math.abs(lefts[i] - lefts[i - 1] - P.pitch) < 1e-9, `teeth ${i - 1}->${i} are ${(lefts[i] - lefts[i - 1]).toFixed(4)} mm apart, not ${P.pitch}`); } }); ok('every slot between two teeth is exactly half the pitch wide', () => { const lefts = boxes.filter((b) => b[2] > P.baseT - 1e-9).map((b) => b[0]).sort((a, b) => a - b); for (let i = 1; i < lefts.length; i++) { // Scan the window that holds one slot and nothing else. const s = clearSpanX(boxes, yMid, z, lefts[i - 1], lefts[i] + meta.toothT); assert(Math.abs(s.width - P.pitch / 2) < 0.01, `slot ${i}: clear span ${s.width.toFixed(3)} mm, expected ${(P.pitch / 2).toFixed(2)}`); } }); ok('the teeth are as thick as the slots, so neither member is the weak one', () => { for (const b of boxes) { if (b[2] < P.baseT - 1e-9) continue; // the base slab assert(Math.abs((b[3] - b[0]) - P.pitch / 2) < 1e-9, `a tooth is ${(b[3] - b[0]).toFixed(3)} mm thick, expected ${(P.pitch / 2).toFixed(2)}`); } }); ok('the default comb lies inside the breadboard bay of the plate on the same page', () => { // This is what makes the published tooth count derived rather than chosen: it is the most // that fits the bay benchPlate() already builds. const bay = benchPlate().meta; assert(meta.W <= bay.bbBayW + 1e-9, `comb is ${meta.W} mm long, bay is ${bay.bbBayW}`); assert(meta.D <= bay.bbBayD + 1e-9, `comb is ${meta.D} mm deep, bay is ${bay.bbBayD}`); // ...and it is the LONGEST one that does: one more tooth overruns the bay, which is why // the generator refuses to build it at all. const oneMore = (2 * (meta.teeth + 1) - 1) * meta.toothT + 2 * P.margin; assert(oneMore > bay.bbBayW, `one more tooth would still fit (${oneMore} mm) — the default is not the longest comb the bay holds`); assert.throws(() => jumperComb({ teeth: meta.teeth + 1 }), /breadboard bay/); }); ok('bounds() agrees with the meta the generator reported', () => { const s = m.bounds(); assert(Math.abs(s.size[0] - meta.W) < 1e-9, `width ${s.size[0]} vs meta ${meta.W}`); assert(Math.abs(s.size[1] - meta.D) < 1e-9, `depth ${s.size[1]} vs meta ${meta.D}`); assert(Math.abs(s.size[2] - meta.H) < 1e-9, `height ${s.size[2]} vs meta ${meta.H}`); assert(Math.abs(s.lo[2]) < 1e-9, `does not sit on Z=0 (lo.z=${s.lo[2]})`); }); ok('nothing overhangs: every tooth stands on the base', () => { for (const b of boxes) { if (b[2] < 1e-9) continue; assert(Math.abs(b[2] - P.baseT) < 1e-9, `a solid starts at z=${b[2]}, not on the base`); } }); ok('the mesh is valid and no two solids interpenetrate', () => { const v = m.check(); assert(v.ok, `mesh invalid: ${v.problems.join('; ')}`); assert(v.solids === meta.teeth + 1, `${v.solids} solids, expected ${meta.teeth} teeth plus one base`); }); } /* A COMB OF A DIFFERENT LENGTH. The tooth count is the whole parameter, so a comb built at some other count has to hold the same pitch and the same mesh guarantees, and the counts that cannot work have to be refused rather than silently produced. */ console.log('\njumper-wire comb, at a tooth count the caller chose'); { const teeth = 8; const { m, meta } = jumperComb({ teeth }); const z = P.baseT + P.wallH / 2; const yMid = P.margin + meta.toothD / 2; ok('a shorter comb keeps the pitch and stays a valid mesh', () => { assert(meta.teeth === teeth && meta.slots === teeth - 1, `${meta.teeth} teeth / ${meta.slots} slots`); const lefts = m.boxes.filter((b) => b[2] > P.baseT - 1e-9).map((b) => b[0]).sort((a, b) => a - b); assert(lefts.length === teeth, `${lefts.length} teeth in the solid`); for (let i = 1; i < lefts.length; i++) { assert(Math.abs(lefts[i] - lefts[i - 1] - P.pitch) < 1e-9, `tooth ${i} is off pitch`); const s = clearSpanX(m.boxes, yMid, z, lefts[i - 1], lefts[i] + meta.toothT); assert(Math.abs(s.width - P.pitch / 2) < 0.01, `slot ${i}: ${s.width.toFixed(3)} mm`); } const v = m.check(); assert(v.ok, `mesh invalid: ${v.problems.join('; ')}`); }); ok('the overall size is the tooth count and the pitch, and nothing else', () => { // (2*teeth - 1) half-pitches of comb, plus one margin at each end. const expect = (2 * teeth - 1) * (P.pitch / 2) + 2 * P.margin; assert(Math.abs(meta.W - expect) < 1e-9, `${meta.W} mm wide, expected ${expect}`); assert(Math.abs(m.bounds().size[0] - expect) < 1e-9, 'the finished solid disagrees with that'); }); ok('counts that cannot work are refused, not quietly built', () => { for (const bad of [1, 0, -3, 2.5, 'six']) { assert.throws(() => jumperComb({ teeth: bad }), /whole number/, `teeth=${JSON.stringify(bad)} was accepted`); } const max = jumperComb().meta.maxTeeth; assert.throws(() => jumperComb({ teeth: max + 1 }), /breadboard bay/, `teeth=${max + 1} was accepted`); assert.doesNotThrow(() => jumperComb({ teeth: max }), 'the stated maximum was refused'); }); ok('a comb of another length does not disturb the published one', () => { assert.strictEqual(jumperComb().meta.teeth, jumperComb().meta.maxTeeth); assert.strictEqual(jumperComb().meta.W, 84.01); }); } console.log(`\n${pass} assertion(s) passed${process.exitCode ? ', SOME FAILED' : ''}`);