From 37795d9f90a78966bf12218d5881dc8b488c39cc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 10:24:54 +0000 Subject: [PATCH 01/58] Cut screw holes and counterbores no narrower than the size asked A plate's screw counterbore is 14-sided and its shank 12-sided, and both had their corners on the size typed, so across their flats they were narrower than it at every size: the default 6 mm counterbore was 5.85 mm and the 3 mm hole 2.90. Now their corners go out until the flats stand on the size, as #61 did for the magnet pocket. No allowance on top: a screw head drops into its counterbore rather than being pressed in. The limits measure the pocket as it is now cut, so each screw size is the room times cos(pi/12) for the hole and cos(pi/14) for the head. At 42 mm the widest hole goes from 8.3 to 8 mm and the widest head from 14 to 13.6; the cap between a cell's four pockets from 25 to 24.3 (head) and 24.1 (hole); the default 3 mm hole now needs a 36.81 mm pitch rather than 36.7, and the 6 mm head 34.16 rather than 34. Designs without screws are unchanged. The plate audit's pinned digest for magnets and screws moves with the pocket, and the 22.2 mm shank row keeps the pocket it was written about (22.2 mm across its corners). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 16 +++++++++++--- src/core.js | 35 ++++++++++++++++++++----------- test/plate-audit.js | 22 ++++++++++++------- test/ui/plates-robustness.spec.js | 19 +++++++++-------- 4 files changed, 60 insertions(+), 32 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 0b03935..bdc4885 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -692,6 +692,15 @@ currently installed): thick walls, scoops, shelves and notes, holes, removable plates and half sizes, it came to 0.998 to 1.001, the lowest those whose shelf has a note raised on it, whose letters it leaves out. +- **A mounting pocket's size is the size across its flats.** Every bore is a polygon, 14 + sides for a magnet and a counterbore and 12 for a shank, and one with its corners on + the size asked is narrower than that across its flats: the default 6 mm counterbore was + 5.85 mm, the 3 mm shank 2.90, a 10 mm magnet's pocket 9.94 even with its 0.1 mm press + fit at the corners. So `fastenerCutter` stands the corners 1/cos(π/n) of the radius out + and the flats on it, and the magnet keeps its 0.1 at the corners where that is further, + up to 7.77 mm. A screw gets no allowance on top: its head drops into the counterbore, + which the field already sizes with room round it. `mountLimits` measures every room to + the corners, so the largest size it takes is that room times cos(π/n). - **The socket's corner clearance is not uniform.** Known, documented, deliberately not fixed — see [socket-clearance.md](socket-clearance.md). - **The plate's outer corner radius has a ceiling, and it is the socket's.** Both the @@ -799,9 +808,10 @@ currently installed): the cell's side where it crosses it, and that side stands on the same plane as the neighbour's, so a split that lands on a corner of the neighbour's region is that corner's edge in both shells (a 21.7 mm magnet from beneath at 55 mm, 0.03 microns off; - a 22.2 mm screw shank at 56.5 mm, on main too). With pockets on the piece, every cell - and margin is checked against the shells built beside it (`settle`), and the pockets of - one or the other are cut again moved 1.7 microns, turned a 28th of a turn, or both. The + a screw shank 22.2 mm across its corners at 56.5 mm, on main too). With pockets on the + piece, every cell and margin is checked against the shells built beside it (`settle`), + and the pockets of one or the other are cut again moved 1.7 microns, turned a 28th of a + turn, or both. The cut kept is the one that shares the fewest edges with every shell beside it: keeping one that touched fewer shells, as `settle` first did, left a cell built later sharing an edge nothing cleared (10.1 to 11.1 mm magnets at 47.91 to 50.05 mm, clean on main). And once diff --git a/src/core.js b/src/core.js index 36e5684..3e2f8d7 100644 --- a/src/core.js +++ b/src/core.js @@ -846,10 +846,18 @@ function fastenerCutter(cfg, magZ0, magZ1, shankTop) { add(cfg.magnetSide === 'top' ? cylinder(0, 0, magnetR, magZ0, magZ1, 14) : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, 14)); + /* A screw's two bores had their corners on the size asked, so their flats came inside + it at every size: the 14-sided counterbore cos(π/14) of the head and the 12-sided + shank cos(π/12) of the hole, 5.85 mm across the flats for the default 6 mm head and + 2.90 for the 3 mm shank. Their corners go out the same way, so the flats stand on the + radius and neither bore is narrower anywhere than the size typed. And no more than + that: the magnet's 0.1 is a press fit, and a screw head is not pressed in. It drops + into its counterbore, which the field already sizes with room round it (6 mm for an + M3's 5.5 mm head), and the shank is a clearance hole. */ if (cfg.screws) { if (cfg.screwHeadD > cfg.screwHoleD) - add(cylinder(0, 0, cfg.screwHeadD/2, -0.5, cfg.screwHeadDepth, 14)); - add(cylinder(0, 0, cfg.screwHoleD/2, -0.5, shankTop, 12)); + add(cylinder(0, 0, cfg.screwHeadD/2 / Math.cos(Math.PI/14), -0.5, cfg.screwHeadDepth, 14)); + add(cylinder(0, 0, cfg.screwHoleD/2 / Math.cos(Math.PI/12), -0.5, shankTop, 12)); } return cut; } @@ -865,13 +873,15 @@ function movePolys(polys, dx, dy) { })); } -// counterbore: hole cylinder full height + wider recess from chosen face +// counterbore: hole cylinder full height + wider recess from chosen face, each with its +// flats on its size as fastenerCutter cuts them function screwCutter(cx, cy, holeD, headD, z0, z1, headDepth, fromTop) { - let polys = cylinder(cx, cy, holeD/2, z0 - 0.5, z1 + 0.5, 12); + let polys = cylinder(cx, cy, holeD/2 / Math.cos(Math.PI/12), z0 - 0.5, z1 + 0.5, 12); if (headD > holeD) { + const headR = headD/2 / Math.cos(Math.PI/14); const rec = fromTop - ? cylinder(cx, cy, headD/2, z1 - headDepth, z1 + 0.5, 14) - : cylinder(cx, cy, headD/2, z0 - 0.5, z0 + headDepth, 14); + ? cylinder(cx, cy, headR, z1 - headDepth, z1 + 0.5, 14) + : cylinder(cx, cy, headR, z0 - 0.5, z0 + headDepth, 14); polys = csgUnion(polys, rec); } return polys; @@ -2483,9 +2493,10 @@ function mountLimits(cfg, layout) { // a magnet pocket's corners are 0.1 mm over the magnet's radius, or out to where its // flats stand on that radius if that is further (fastenerCutter) magnetD: r10(2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14))), - // a screw's bores have their corners on its size - screwHoleD: r10(2 * Math.min(fit(top), below)), - screwHeadD: r10(2 * Math.min(fit(under), below)), + // a screw's bores have their flats on its size, so their corners stand 1/cos(π/n) of + // it out: the shank's 12 sides and the counterbore's 14 (fastenerCutter) + screwHoleD: r10(2 * Math.min(fit(top), below) * Math.cos(Math.PI / 12)), + screwHeadD: r10(2 * Math.min(fit(under), below) * Math.cos(Math.PI / 14)), // in the solid floor the pad grows to suit, so only a boss caps the depth depth: bosses ? r10(BOSS_H - MOUNT_SKIN) : Infinity, // which of the sizes the cut beside it stops, rather than the floor, cell or boss @@ -3111,9 +3122,9 @@ function buildPiece(cfg, layout, piece, onStatus) { * A 21.7 mm magnet from beneath at a 55 mm pitch did it: a flat of the pocket in the * cell at the back left, on the magnet's radius, crosses that cell's front side 0.03 * microns from the corner of the cell at the back right. A 22.2 mm screw shank at 56.5 - * mm did it twice over, from the cell at the back right as well, on main too. Each shell - * is closed on its own, so the pockets' own retries never see it, and it comes and goes - * with the size and the pitch. + * mm (22.2 across its corners, as shanks were cut then) did it twice over, from the cell + * at the back right as well, on main too. Each shell is closed on its own, so the + * pockets' own retries never see it, and it comes and goes with the size and the pitch. * * So with mounting pockets on the piece, every shell the cells and margins make is * checked against those built before it beside it, as jointed cells are above. One that diff --git a/test/plate-audit.js b/test/plate-audit.js index 18eb9bb..c7fdcf5 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -1945,12 +1945,14 @@ console.log('\nthe other limits, built at their ends:'); half a MOUNT_WALL short of the line halfway to the one beside it, so its corners stop at holeOffset less 0.5 and the wall between two pockets is at least 1 mm. A magnet pocket's corners are at the larger of 0.1 over the magnet's radius and - the radius over cos(π/14) (its flats on the magnet); a screw's bores have their corners - on its size. At 55 and 60 mm, each way in: the widest the page takes has to build - watertight, and the first tenth past the rule has to be refused. */ + the radius over cos(π/14) (its flats on the magnet); a screw's bores have their flats + on its size too, so their corners are its radius over cos(π/12) for the 12-sided shank + and cos(π/14) for the 14-sided head. At 55 and 60 mm, each way in: the widest the page + takes has to build watertight, and the first tenth past the rule has to be refused. */ const reach = G.DEFAULTS.holeOffset - 0.5; const corner = { magnetD: (d) => Math.max(d / 2 + 0.1, d / 2 / Math.cos(Math.PI / 14)), - screwHoleD: (d) => d / 2, screwHeadD: (d) => d / 2 }; + screwHoleD: (d) => d / 2 / Math.cos(Math.PI / 12), + screwHeadD: (d) => d / 2 / Math.cos(Math.PI / 14) }; const firstPast = (f) => { let d = 1; while (corner[f](d) <= reach + 1e-9) d = Math.round(d * 10 + 1) / 10; return d; }; const WIDE = [ ['magnet from below', { magnets: true }, 'magnetD'], @@ -1988,9 +1990,12 @@ console.log('\nthe other limits, built at their ends:'); sliver of the socket floor by a corner 4.7 microns from one of the floor's spokes: 11.1 mm at 48.55, 24 open edges on d9442d4. Main's pocket has a corner 1.0 micron from the same spoke and none open, so it is how the cut falls, not how near. A cell - open at the floor's height has its floor fanned again (fanCentre). */ + open at the floor's height has its floor fanned again (fanCentre). + The shank is the pocket it was then, 22.2 mm across its corners: since a screw's + bores stand their flats on the size asked, that is the shank a 21.44 mm hole cuts. */ + const shank222 = 22.2 * Math.cos(Math.PI / 12); for (const [nm, o] of [['21.7 mm magnet at 55 mm', { pitch: 55, magnets: true, magnetD: 21.7 }], - ['22.2 mm screw shank at 56.5', { pitch: 56.5, screws: true, screwHoleD: 22.2, screwHeadD: 22.2 }], + ['22.2 mm screw shank at 56.5', { pitch: 56.5, screws: true, screwHoleD: shank222, screwHeadD: shank222 }], ['10.1 mm magnet above, 47.91', { pitch: 47.91, magnets: true, magnetD: 10.1, magnetSide: 'top' }], ['10.6 mm magnet below, 48.98', { pitch: 48.98, magnets: true, magnetD: 10.6 }], ['11.1 mm magnet above, 48.55', { pitch: 48.55, magnets: true, magnetD: 11.1, magnetSide: 'top' }]]) { @@ -2468,9 +2473,10 @@ console.log('\nplates without half cells build the same bytes:'); alignX: 'end', alignY: 'end', connector: 'none' }, '37b5f305cc4da6dd'], // fanCentre moved four of its nine cells' bottom caps off a spoke a few thousandths from // a cutter's corner: the same bottom face fanned from another point, its area and the - // volume unchanged + // volume unchanged. Then the screw's bores went out until their flats stood on the + // sizes asked, the counterbore and the shank wider by design ['126 x 126, magnets and screws', { drawerW: 126, drawerD: 126, marginMode: 'custom', - mLeft: 0, mRight: 0, mFront: 0, mBack: 0, magnets: true, screws: true }, '5b339b75bd8c513e'], + mLeft: 0, mRight: 0, mFront: 0, mBack: 0, magnets: true, screws: true }, '7eaec017027d5a46'], ['140 x 140, corner pockets', { drawerW: 140, drawerD: 140, marginMode: 'auto', magnets: true, baseMode: 'bosses' }, '7088f24def428095'], ['168 x 180, skeleton', { drawerW: 168, drawerD: 180, marginMode: 'auto', diff --git a/test/ui/plates-robustness.spec.js b/test/ui/plates-robustness.spec.js index db5c058..ef96ee4 100644 --- a/test/ui/plates-robustness.spec.js +++ b/test/ui/plates-robustness.spec.js @@ -97,10 +97,10 @@ test.describe('ranges on the geometry fields', () => { ['#pi=55&w=110&d=110&mm=custom&ml=0&mr=0&mf=0&mb=0&bw=400&bd=400&mg=1&md=25.4', 'errMagnet', /Magnet Ø must be 24\.3 mm or less at a 55 mm pitch — mounting holes sit 13 mm from each cell centre, where the Gridfinity spec puts them, and a cell's four holes have to stay clear of each other/], ['#pi=60&w=120&d=120&mm=custom&ml=0&mr=0&mf=0&mb=0&bw=400&bd=400&sc=1&sd=26', 'errScrew', - /Screw head Ø must be 25 mm or less at a 60 mm pitch — .* a cell's four holes have to stay clear of each other/], + /Screw head Ø must be 24\.3 mm or less at a 60 mm pitch — .* a cell's four holes have to stay clear of each other/], ['#mg=1&bm=bosses&mh=3', 'errMagnet', /Magnet depth must be 2\.4 mm or less with corner pockets/], - ['#sc=1&sh=20', 'errScrew', /Screw hole Ø must be 8\.3 mm or less/], - ['#cn=none&sc=1&sd=30', 'errScrew', /Screw head Ø must be 14 mm or less/], + ['#sc=1&sh=20', 'errScrew', /Screw hole Ø must be 8 mm or less/], + ['#cn=none&sc=1&sd=30', 'errScrew', /Screw head Ø must be 13\.6 mm or less/], ['#sc=1&se=50', 'errScrew', /Screw head depth must be 10 mm or less/], ['#cl=1', 'errConnClr', /Fit clearance must be 0\.3 mm or less — any looser and a dovetail pocket/], ['#cn=bowtie&cl=5', 'errConnClr', /Fit clearance must be 1 mm or less — check the figure is in millimetres/], @@ -114,7 +114,7 @@ test.describe('ranges on the geometry fields', () => { [`${BOWTIE_42}&mg=1&ms=top&md=8`, 'errMagnet', /Magnet Ø must be 7\.6 mm or less at a 42 mm pitch — .* the recesses the bowtie keys fit into/], [`${BOWTIE_42.replace('cn=bowtie', 'cn=snap')}&sc=1&sd=10`, 'errScrew', - /Screw head Ø must be 7\.8 mm or less at a 42 mm pitch — .* the recesses the snap clips fit into/], + /Screw head Ø must be 7\.6 mm or less at a 42 mm pitch — .* the recesses the snap clips fit into/], ['#mg=1&md=20', 'errMagnet', /Magnet Ø must be 12\.3 mm or less at a 42 mm pitch — .* the notches the dovetail tabs fit into/], ]; @@ -385,12 +385,13 @@ test.describe('limits no tighter than the geometry', () => { expect(s.built).toBeCloseTo(7.05, 9); }); - /* The 34 mm case takes no joint: the default dovetail's notches hold its 6 mm screw head - to 3.8 mm there, which is a field error of its own. The puzzle tabs' are what leave - a magnet no room at 36 mm, where the cell alone would take 7.7. */ + /* The 34.5 mm case takes no joint: the default dovetail's notches hold its 6 mm screw + head to 4.1 mm there, which is a field error of its own. Nor 34 mm, where the head's + flats on its size leave the cell room for 5.8. The puzzle tabs' are what leave a + magnet no room at 36 mm, where the cell alone would take 7.7. */ for (const [hash, id, errId, carried, msg] of [ - ['#cn=none&sc=1&pi=34', 'screwHoleD', 'errScrew', 3, - /Screw hole Ø: there is no room for one at a 34 mm pitch.*Use a larger pitch, or turn off screw holes\./], + ['#cn=none&sc=1&pi=34.5', 'screwHoleD', 'errScrew', 3, + /Screw hole Ø: there is no room for one at a 34\.5 mm pitch.*Use a larger pitch, or turn off screw holes\./], ['#mg=1&pi=20', 'magnetD', 'errMagnet', 6, /Magnet Ø: there is no room for one at a 20 mm pitch.*Use a larger pitch, or turn off magnet pockets\./], ['#cn=puzzle&mg=1&pi=36', 'magnetD', 'errMagnet', 6, From 7007724f3dab285fb6b2e42112fcd287a9552b05 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 10:41:44 +0000 Subject: [PATCH 02/58] Decide each mounting bore's shape in one place, for the cutters and the limits alike MOUNT_BORE holds each bore's sides, how far its corners stand for a size, and the largest size whose corners fit a room. fastenerCutter and screwCutter cut what it says, and mountLimits reads its sizes back from it, so a pocket and its limit cannot drift apart. Nothing built changes: the same bytes for the plates tried, and the same limits over 19,404 configurations. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 12 ++++---- src/core.js | 80 +++++++++++++++++++++++++++++--------------------- 2 files changed, 54 insertions(+), 38 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index bdc4885..13397bb 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -696,11 +696,13 @@ currently installed): sides for a magnet and a counterbore and 12 for a shank, and one with its corners on the size asked is narrower than that across its flats: the default 6 mm counterbore was 5.85 mm, the 3 mm shank 2.90, a 10 mm magnet's pocket 9.94 even with its 0.1 mm press - fit at the corners. So `fastenerCutter` stands the corners 1/cos(π/n) of the radius out - and the flats on it, and the magnet keeps its 0.1 at the corners where that is further, - up to 7.77 mm. A screw gets no allowance on top: its head drops into the counterbore, - which the field already sizes with room round it. `mountLimits` measures every room to - the corners, so the largest size it takes is that room times cos(π/n). + fit at the corners. So every bore stands its corners 1/cos(π/n) of the radius out and + its flats on it, and the magnet keeps its 0.1 at the corners where that is further, up + to 7.77 mm. A screw gets no allowance on top: its head drops into the counterbore, + which the field already sizes with room round it. `MOUNT_BORE` says each bore's sides + and corner radius, and the largest size whose corners fit a room; `fastenerCutter` cuts + what it says, and `mountLimits`, which measures every room to the corners, reads its + sizes back from it, so the shape is decided in one place. - **The socket's corner clearance is not uniform.** Known, documented, deliberately not fixed — see [socket-clearance.md](socket-clearance.md). - **The plate's outer corner radius has a ceiling, and it is the socket's.** Both the diff --git a/src/core.js b/src/core.js index 3e2f8d7..2bf36e8 100644 --- a/src/core.js +++ b/src/core.js @@ -813,6 +813,36 @@ function cylinder(cx, cy, r, z0, z1, seg) { return polys; } +/* The polygon each mounting bore is cut as: how many sides, and how far out its corners + * stand for a size typed (`r`), and back the other way, the largest size whose corners + * stay inside a room (`fits`). fastenerCutter and screwCutter cut these and mountLimits + * holds the sizes to them, so a bore's shape is decided here and nowhere else. Each is + * cylinder's polygon about the site, a corner at every 2πk/sides from +x. + * + * Every bore stands its flats on the size asked, so its corners are 1/cos(π/sides) of the + * radius out and it is no narrower anywhere than the size typed. + * - magnet: 14 sides, cut 0.1 mm over the magnet's radius at its corners: 0.2 over the + * diameter, the press fit bins/bin.js keeps as well ("the plate's rule"). Its flats + * are cos(π/14) of that, and past a 7.78 mm magnet they came inside the magnet + * itself: 4.972 mm for a 10 mm one, a pocket narrower than what goes in it, where + * bins/bin.js's 16 sides keep their flats outside every magnet its field takes. So + * the corners go out as far as it takes for the flats to stand on the magnet's + * radius. For a magnet up to 7.77 mm, the 6 mm default among them, nothing moves. + * - head, the counterbore: 14 sides, and hole, the shank: 12. Both had their corners on + * the size asked, so their flats came inside it at every size, 5.85 mm across for the + * default 6 mm head and 2.90 for the 3 mm hole. Now their corners go out the same + * way, and no further: the magnet's 0.1 is a press fit, and a screw head is not + * pressed in. It drops into its counterbore, which the field already sizes with room + * round it (6 mm for an M3's 5.5 mm head), and the shank is a clearance hole. */ +const MOUNT_BORE = { + magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), + fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, + head: { sides: 14, r: (d) => d/2 / Math.cos(Math.PI/14), + fits: (room) => 2 * room * Math.cos(Math.PI / 14) }, + hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), + fits: (room) => 2 * room * Math.cos(Math.PI / 12) }, +}; + /* Everything removed at one mounting site, as ONE solid. * * Up to three cylinders share this axis: a magnet pocket, a screw shank, and the head's @@ -834,30 +864,17 @@ function cylinder(cx, cy, r, z0, z1, seg) { function fastenerCutter(cfg, magZ0, magZ1, shankTop) { let cut = null; const add = (c) => { cut = cut ? csgUnion(cut, c) : c; }; - /* A magnet's pocket is 14-sided, cut 0.1 mm over the magnet's radius at its corners: - 0.2 over the diameter, the press fit bins/bin.js keeps as well ("the plate's rule"). - Its flats are cos(π/14) of that, and past a 7.78 mm magnet they came inside the - magnet itself: 4.972 mm for a 10 mm one, a pocket narrower than what goes in it, - where bins/bin.js's 16 sides keep their flats outside every magnet its field takes. - So the corners go out as far as it takes for the flats to stand on the magnet's - radius. For a magnet up to 7.77 mm, the 6 mm default among them, nothing moves. */ - const magnetR = Math.max(cfg.magnetD/2 + 0.1, cfg.magnetD/2 / Math.cos(Math.PI/14)); + // each bore as MOUNT_BORE shapes it + const { magnet, head, hole } = MOUNT_BORE; + const magnetR = magnet.r(cfg.magnetD); if (cfg.magnets) add(cfg.magnetSide === 'top' - ? cylinder(0, 0, magnetR, magZ0, magZ1, 14) - : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, 14)); - /* A screw's two bores had their corners on the size asked, so their flats came inside - it at every size: the 14-sided counterbore cos(π/14) of the head and the 12-sided - shank cos(π/12) of the hole, 5.85 mm across the flats for the default 6 mm head and - 2.90 for the 3 mm shank. Their corners go out the same way, so the flats stand on the - radius and neither bore is narrower anywhere than the size typed. And no more than - that: the magnet's 0.1 is a press fit, and a screw head is not pressed in. It drops - into its counterbore, which the field already sizes with room round it (6 mm for an - M3's 5.5 mm head), and the shank is a clearance hole. */ + ? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) + : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { if (cfg.screwHeadD > cfg.screwHoleD) - add(cylinder(0, 0, cfg.screwHeadD/2 / Math.cos(Math.PI/14), -0.5, cfg.screwHeadDepth, 14)); - add(cylinder(0, 0, cfg.screwHoleD/2 / Math.cos(Math.PI/12), -0.5, shankTop, 12)); + add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); + add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); } return cut; } @@ -873,15 +890,15 @@ function movePolys(polys, dx, dy) { })); } -// counterbore: hole cylinder full height + wider recess from chosen face, each with its -// flats on its size as fastenerCutter cuts them +// counterbore: hole cylinder full height + wider recess from chosen face, each bore as +// MOUNT_BORE shapes it function screwCutter(cx, cy, holeD, headD, z0, z1, headDepth, fromTop) { - let polys = cylinder(cx, cy, holeD/2 / Math.cos(Math.PI/12), z0 - 0.5, z1 + 0.5, 12); + const { head, hole } = MOUNT_BORE; + let polys = cylinder(cx, cy, hole.r(holeD), z0 - 0.5, z1 + 0.5, hole.sides); if (headD > holeD) { - const headR = headD/2 / Math.cos(Math.PI/14); const rec = fromTop - ? cylinder(cx, cy, headR, z1 - headDepth, z1 + 0.5, 14) - : cylinder(cx, cy, headR, z0 - 0.5, z0 + headDepth, 14); + ? cylinder(cx, cy, head.r(headD), z1 - headDepth, z1 + 0.5, head.sides) + : cylinder(cx, cy, head.r(headD), z0 - 0.5, z0 + headDepth, head.sides); polys = csgUnion(polys, rec); } return polys; @@ -2490,13 +2507,10 @@ function mountLimits(cfg, layout) { const own = fit(cfg.magnetSide === 'top' ? top : under); const room = Math.min(own, magnetJoint); return { - // a magnet pocket's corners are 0.1 mm over the magnet's radius, or out to where its - // flats stand on that radius if that is further (fastenerCutter) - magnetD: r10(2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14))), - // a screw's bores have their flats on its size, so their corners stand 1/cos(π/n) of - // it out: the shank's 12 sides and the counterbore's 14 (fastenerCutter) - screwHoleD: r10(2 * Math.min(fit(top), below) * Math.cos(Math.PI / 12)), - screwHeadD: r10(2 * Math.min(fit(under), below) * Math.cos(Math.PI / 14)), + // every room above is to a bore's corners, which stand out past its size (MOUNT_BORE) + magnetD: r10(MOUNT_BORE.magnet.fits(room)), + screwHoleD: r10(MOUNT_BORE.hole.fits(Math.min(fit(top), below))), + screwHeadD: r10(MOUNT_BORE.head.fits(Math.min(fit(under), below))), // in the solid floor the pad grows to suit, so only a boss caps the depth depth: bosses ? r10(BOSS_H - MOUNT_SKIN) : Infinity, // which of the sizes the cut beside it stops, rather than the floor, cell or boss From 5b0dda0498ace00084738d0625a1875064e8a23a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 11:23:13 +0000 Subject: [PATCH 03/58] Explain why the no-room screw check moved to 34.5 mm, and tidy the settle note Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 22 +++++++++++----------- test/ui/plates-robustness.spec.js | 8 +++++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 13397bb..237570d 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -813,17 +813,17 @@ currently installed): a screw shank 22.2 mm across its corners at 56.5 mm, on main too). With pockets on the piece, every cell and margin is checked against the shells built beside it (`settle`), and the pockets of one or the other are cut again moved 1.7 microns, turned a 28th of a - turn, or both. The - cut kept is the one that shares the fewest edges with every shell beside it: keeping one - that touched fewer shells, as `settle` first did, left a cell built later sharing an edge - nothing cleared (10.1 to 11.1 mm magnets at 47.91 to 50.05 mm, clean on main). And once - the cells are built, if the shells `settle` cut again have more bad edges with those - beside them than they started with, or more open or turned over, they all go back to - their first cut. That check sees only those shells and their neighbours as the cells - left them: the dovetail and puzzle tabs are made after it, and the top-insert pass cuts - its housings out of shells after it, so neither is judged by it. Two cups put in - from above from each side of a piece one cell deep meet face to face at 14.44 mm (twice a - cup's reach), and there they are built as one solid with `csgUnion`. + turn, or both. The cut kept is the one that shares the fewest edges with every shell + beside it: keeping one that touched fewer shells, as `settle` first did, left a cell + built later sharing an edge nothing cleared (10.1 to 11.1 mm magnets at 47.91 to + 50.05 mm, clean on main). And once the cells are built, if the shells `settle` cut + again have more bad edges with those beside them than they started with, or more open + or turned over, they all go back to their first cut. That check sees only those shells + and their neighbours as the cells left them: the dovetail and puzzle tabs are made + after it, and the top-insert pass cuts its housings out of shells after it, so neither + is judged by it. Two cups put in from above from each side of a piece one cell deep + meet face to face at 14.44 mm (twice a cup's reach), and there they are built as one + solid with `csgUnion`. - **A face that is nothing but a straight line has no middle.** A weld that closes a T-junction can leave three corners on one line. Fanned from its average, the spokes land a fraction of a micron from a corner and read, to `checkManifold`'s thousandths, as the diff --git a/test/ui/plates-robustness.spec.js b/test/ui/plates-robustness.spec.js index ef96ee4..177de84 100644 --- a/test/ui/plates-robustness.spec.js +++ b/test/ui/plates-robustness.spec.js @@ -386,9 +386,11 @@ test.describe('limits no tighter than the geometry', () => { }); /* The 34.5 mm case takes no joint: the default dovetail's notches hold its 6 mm screw - head to 4.1 mm there, which is a field error of its own. Nor 34 mm, where the head's - flats on its size leave the cell room for 5.8. The puzzle tabs' are what leave a - magnet no room at 36 mm, where the cell alone would take 7.7. */ + head to 4.1 mm there, which is a field error of its own. It was 34 mm until a head + was cut with its flats on its size; at 34 the cell now has room for a 5.8 mm head, + so the 6 mm one would be a field error too, and 34.5 still has no room for a shank. + The puzzle tabs' are what leave a magnet no room at 36 mm, where the cell alone + would take 7.7. */ for (const [hash, id, errId, carried, msg] of [ ['#cn=none&sc=1&pi=34.5', 'screwHoleD', 'errScrew', 3, /Screw hole Ø: there is no room for one at a 34\.5 mm pitch.*Use a larger pitch, or turn off screw holes\./], From c72699e282c487cd97f783c3c976d7d5de91d45a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 11:44:17 +0000 Subject: [PATCH 04/58] Let a bowtie in the floor take the full fit clearance on small pitches, and say what holds the other ceilings Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 16 +++++++ src/core.js | 64 ++++++++++++++++++---------- src/ui.js | 16 ++++--- test/plate-audit.js | 71 ++++++++++++++++--------------- test/ui/plates-robustness.spec.js | 22 +++++----- 5 files changed, 117 insertions(+), 72 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 237570d..2e543cb 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -824,6 +824,22 @@ currently installed): is judged by it. Two cups put in from above from each side of a piece one cell deep meet face to face at 14.44 mm (twice a cup's reach), and there they are built as one solid with `csgUnion`. +- **None of the fit clearance's ceilings that waited on a sweep can go up but the + bowtie's.** `connClrCeiling` caps the field for each joint, and five of its ceilings + were set by open edges one step past them that `cutAgain` closes. Swept again on 2 × 2 + and 1-cell pieces, every 0.01 mm of pitch over each ceiling's band, every 0.05 of the + field past it and every 0.01 near a leak: the dovetail's step past its 0.3 is open + still at one pitch (0.35 at 42.35 mm), and the puzzle key in the floor's past its 0.8 + leaks at another (0.85 at 29.74). The puzzle and the puzzle key under 20 mm leak within + two steps of their 0.3 (0.38 at 18.54 mm, 0.37 at 18.07). The puzzle key's and the + puzzle's are no longer holes: the joint's cut lands on the edge of the `BLOAT` band two + cells overlap in, along a line that climbs with the field, and the two shells share + that edge, as the dovetail's do from 0.39. A ceiling goes up a step only where the + field builds clean to the step after it, so all four stay. The bowtie in the floor + under 20 mm leaked nowhere up to 1.05, at every 0.01 mm from 13.5 to 20 and loose and + tight at every 0.1, and takes the plain 1 mm. `test/plate-audit.js` builds each leak, + which has to stay one and be refused, and the bowtie's step to spare, which has to + build closed. - **A face that is nothing but a straight line has no middle.** A weld that closes a T-junction can leave three corners on one line. Fanned from its average, the spokes land a fraction of a micron from a corner and read, to `checkManifold`'s thousandths, as the diff --git a/src/core.js b/src/core.js index 2bf36e8..3e9e3be 100644 --- a/src/core.js +++ b/src/core.js @@ -1021,13 +1021,23 @@ function fitClearances(field) { * for everything but the dovetail let a snap clip's housing into the next piece and let * small pitches leak. * - * Measured on 2 × 2 and 1-cell pieces from 13.5 to 60 mm, every 0.5 mm of pitch — at - * every 0.05 of clearance for the puzzle, the bowtie and the puzzle key, at six for the - * rest — and at every 0.01 of clearance at 20, 25, 30, 35, 42 and 60: + * Measured first on 2 × 2 and 1-cell pieces from 13.5 to 60 mm, every 0.5 mm of pitch — + * at every 0.05 of clearance for the puzzle, the bowtie and the puzzle key, at six for + * the rest — and at every 0.01 of clearance at 20, 25, 30, 35, 42 and 60. What leaked + * there was the sliver class, and buildPiece now cuts a joint again when its cut comes + * out open (cutAgain), so each ceiling was swept again on both layouts: every 0.01 mm of + * pitch over the band it governs (13.5 to 60 mm for the dovetail), every 0.05 of the + * field past it, and every 0.01 near anything that leaked. A ceiling goes up a step only + * where the field builds clean to the step after that one, which is then its step to + * spare; anything that leaks within two steps of a ceiling earns it. * - * dovetail 0.3. Past it the pocket, cut 1.9 mm + this into the piece, breaks through - * the 2.15 mm socket wall and crosses the chamfer cone; it leaked from 0.34. - * The hint's own advice stops at 0.25. + * dovetail 0.3, earned. The pocket, cut 1.9 mm + this into the piece, breaks through + * the 2.15 mm socket wall from 0.25 and crosses the chamfer cone. It leaked + * from 0.34, and cutAgain closes most of that, but one step past, 0.35 still + * leaves edges open at 42.35 mm on both layouts. Past that two cells' shells + * share an edge from 0.39, where the notch's wide end stands on the edge of + * the band they share (22.64 mm, and 22.65 at 0.4), and more leak or fold at + * 0.55, 0.75 and 0.8. The hint's own advice stops at 0.25. * snaptop 0.3, in either housing. The clip's slot puts its seam-side wall * 0.3 − (the key's clearance) from the seam, so past a field of 0.35 that * wall stands in the next piece — by 0.65 mm at 1 — and two pieces that print @@ -1038,15 +1048,23 @@ function fitClearances(field) { * pair did the same for any field from 0.3 up. 0.3 holds the wall one BLOAT, * 0.05, inside the piece, the overlap the cell regions keep; the coupon's * slackest pair is this ceiling cut the key's way, so it stops there too. - * pitch 0.3 below 20 mm, for the three housings the field cuts that leaked there: - * the puzzle, and the bowtie and puzzle key in the floor. Past 0.3 they left - * holes from 13.5 to 19 mm — a puzzle key at 14 from 0.4, a bowtie at 18 at - * 1, a puzzle at 19 from 0.35 — and from the default 0.2 to 0.3 none did. The - * puzzle's lobe also needs room in its cell: it opens the plate once its - * clearance passes half the pitch less 6.475 mm, so it stops at half the pitch - * less 6.5 — 0.25 at 13.5, and 0.3 from 13.6. - * joint 0.8 for a puzzle key in the floor from 20 mm up: between 0.82 and 0.96 its - * recess leaked at every pitch measured, 20 to 60. + * pitch 0.3 below 20 mm for the puzzle and a puzzle key in the floor, earned. Past + * it they left holes from 13.5 to 19 mm — a puzzle key at 14 from 0.4, a + * puzzle at 19 from 0.35 — which cutAgain closes, and 0.35 builds clean at + * every pitch; but on 1-cell pieces a line across the pitch that climbs with + * the field puts the cut on the edge of the band two cells share, and the + * shells share it: a puzzle key from 0.37 (at 18.07 mm), a puzzle tab from + * 0.38 (at 18.54). The puzzle's lobe also needs room in its cell: it opens the + * plate once its clearance passes half the pitch less 6.475 mm, so it stops at + * half the pitch less 6.5 — 0.25 at 13.5, and 0.3 from 13.6. A bowtie in the + * floor was held here too, for holes at 18 mm at 1. It builds clean at every + * 0.01 mm from 13.5 to 20 and every 0.05 up to 1.05, on both layouts, and + * loose and tight at every 0.1 mm, so it takes the plain 1 mm. + * joint 0.8 for a puzzle key in the floor from 20 mm up, earned. Its recess leaked + * between 0.82 and 0.96 at every pitch measured, 20 to 60, until cutAgain; + * at 0.85 it now stands on the edge of the band two cells share at 29.74 mm + * on 1-cell pieces, and at 0.86 at 29.79, though nowhere else from 20 to + * 51.7 at 0.85 or 0.9. * slip 1 for the rest. A key in the walls is cut to keySlim's own clearance, which * the field does not move. The snap and the H-clip did not leak above 0.3 at * any pitch measured but in one band — an H-clip dropped in from above leaks @@ -1055,19 +1073,21 @@ function fitClearances(field) { * 0.39–0.4 at 21 and 32.5–35 mm and here and there from 0.52 to 0.87. That is * the sliver class again, not a ceiling — no limit short of 0.38 misses them, * and the same class turns up under the default too: a snap at 20 mm at 0.29, - * a bowtie at 17 mm at 0.15 and under. + * a bowtie at 17 mm at 0.15 and under. One is not isolated: on 2 × 2 pieces + * a puzzle at 0.35 exactly, and not 0.34 or 0.36, has two shells sharing an + * edge at every pitch from 31.6 to 52.2 mm, where a corner of a notch's lobe, + * centred on the line between two cells, stands on the edge of the band they + * share. That wants mending in the cut; no ceiling misses a single value. * - * The leaks that set the pitch and joint ceilings, and the dovetail's at 0.34, were that - * class as well: buildPiece now cuts a joint again when its cut comes out open - * (cutAgain), and the step past each that test/plate-audit.js builds is closed, as is the - * H-clip from above at 0.74. The ceilings stay where they are until a sweep as fine as - * the one that found the small-pitch leaks says how far each can go. */ + * test/plate-audit.js builds the step that earns each ceiling, which has to leak or cross + * the seam and be refused, and the bowtie's step to spare, which has to build closed and + * be refused. */ function connClrCeiling(cfg) { const R = PLATE_RANGES.connClr; const kind = jointKind(cfg.connector, cfg.keyMount, cfg.keyInsert); if (kind === 'dovetail') return { max: R.dovetail, by: 'dovetail' }; if (kind === 'snaptop') return { max: R.snapTop, by: 'snaptop' }; - const floorKey = ['bowtie', 'puzzlekey'].includes(cfg.connector) && cfg.keyMount !== 'wall'; + const floorKey = cfg.connector === 'puzzlekey' && cfg.keyMount !== 'wall'; if (cfg.connector !== 'puzzle' && !floorKey) return { max: R.max, by: 'slip' }; if (cfg.pitch < R.smallPitch) { const room = cfg.connector === 'puzzle' diff --git a/src/ui.js b/src/ui.js index d35fe29..41c5793 100644 --- a/src/ui.js +++ b/src/ui.js @@ -140,15 +140,21 @@ const LIMITS = { /* What sets the clearance's ceiling, in words: core.js connClrCeiling decides it and says which reason applies. A ceiling that moves with the pitch names the pitch, as the mount sizes do, because that is the number to change; 'slip' is the plain 1 mm and keeps the - millimetres advice. */ -const CLR_JOINT = { puzzle: 'puzzle tab', bowtie: 'bowtie key', puzzlekey: 'puzzle key' }; + millimetres advice. The puzzle and the puzzle key said they opened holes in the plate, + as they did until a joint's cut was taken again when it came out open (cutAgain in + core.js); past each ceiling now two cells' shells share an edge at some pitches, which + is not watertight either but is no hole. The dovetail said its pocket broke through + into the socket beside it, which it does from 0.25, under its ceiling. */ +const CLR_JOINT = { puzzle: 'puzzle tab', puzzlekey: 'puzzle key' }; const clrWhy = (by) => ({ - dovetail: '— any looser and a dovetail pocket breaks through into the socket beside it', + dovetail: '— any looser and a dovetail pocket leaves the plate not watertight at some ' + + 'pitches', snaptop: '— any looser and the housing of a snap clip dropped in from above runs up to ' + 'the seam and on into the next piece', pitch: `${atPitch()} — on cells under ${RANGES.connClr.smallPitch} mm a ` + - `looser ${CLR_JOINT[state.connector]} opens holes in the plate`, - joint: `— any looser and a ${CLR_JOINT[state.connector]}'s recess opens holes in the plate`, + `looser ${CLR_JOINT[state.connector]} leaves the plate not watertight`, + joint: `— any looser and a ${CLR_JOINT[state.connector]}'s recess leaves the plate not ` + + 'watertight at some pitches', })[by] || ''; /* id -> the message that goes under it. Rebuilt from scratch on every read, so a field that has come good stops complaining without anything having to remember it once did. */ diff --git a/test/plate-audit.js b/test/plate-audit.js index c7fdcf5..b1d3e52 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -2061,17 +2061,16 @@ console.log('\nthe other limits, built at their ends:'); if (r.bad) bad++; } /* The clearance one step past its end, which leaked, and that was why the end is where - it is. It no longer does: the joint's cut taken again when it comes out open - (cutAgain in core.js) closes it, as it closes the steps past the ceilings below. So it - has to stay closed, and the field has to go on refusing it: the cap stays where it is - until a sweep says how far it can go, the dovetail every 0.01 mm of pitch and every - 0.05 of the field past 0.3, on both piece layouts, clean up to the new cap. */ - const past = buildAll({ ...split, connector: 'dovetail', clr: 0.35 }); + it is. The joint's cut taken again when it comes out open (cutAgain in core.js) + closed it at 42 mm, so it waited on a sweep; the sweep connClrCeiling describes found + 0.35 open still at 42.35 mm, on two of the four pieces, so the cap is earned. It has + to leak still, or the cap can go up, and the field has to go on refusing it. */ + const past = buildAll({ pitch: 42.35, ...PIECE_LAYOUTS['2x2 pieces'](42.35), connector: 'dovetail', clr: 0.35 }); const capped = 0.35 > G.connClrCeiling({ ...G.DEFAULTS, connector: 'dovetail' }).max + 1e-9; - console.log(` ${'dovetail at 0.35 clearance'.padEnd(28)} ${leakText(past)}` + - (!capped ? ' THE FIELD TAKES IT' : past.bad ? ' OPEN AGAIN — cutAgain no longer closes it' - : ' — closed, and the cap waits on a sweep')); - if (!capped || past.bad) bad++; + console.log(` ${'dovetail, 0.35 at 42.35 mm'.padEnd(28)} ${leakText(past)}` + + (!capped ? ' THE FIELD TAKES IT' : !past.bad ? ' NOW CLEAN — the cap can go up' + : ' — the cap is earned')); + if (!capped || !past.bad) bad++; /* A corner boss is 2.6 mm tall and does not grow, so the pocket in it is capped — at what leaves a layer over it, which takes the spec's 6.5 × 2.4 magnet. Built at every @@ -2115,12 +2114,12 @@ console.log('\nthe other limits, built at their ends:'); * pitches between. Each has to come back closed and inside its own width: nothing past * its footprint but the tabs and lobes buildPiece declares. * - * Then the step past each ceiling that set it, which has to be open or across the seam - * still: if the engine closes one, this says that ceiling can go up (four are closed, and - * wait on a sweep to say how far; see PAST below). It has to be past the - * ceiling as well, refused by the field. A puzzle key in the floor loosened to 0.9 built - * clean at 20, 30 and 42 and its step past, 0.82, leaked as before, so a ceiling moved - * over the very number that earned it passed; now the field taking that number fails. + * Then the step that earns each ceiling, which has to be open, touching or across the + * seam still: if the engine closes one, this says that ceiling can go up (see PAST + * below). It has to be past the ceiling as well, refused by the field. A puzzle key in + * the floor loosened to 0.9 built clean at 20, 30 and 42 and its step past, 0.82, leaked + * as before, so a ceiling moved over the very number that earned it passed; now the + * field taking that number fails. * And the fit coupon at each ceiling, which is the same joint at four clearances up to * it — its pairs have to stay at or under the ceiling, and a housing must not reach into * the gap between a pair's two tiles, where it meets the other tile's. @@ -2234,25 +2233,27 @@ console.log('\nthe fit clearance at its ceiling, every joint and pitch band:'); ? `HELD TO IT: ${held.join(', ')}` : `every other joint but the snap from above, at its own ${C.snapTop}`)); if (held.length) bad++; - /* The puzzle key in the floor's step is 0.82, the first clearance that leaked at every - pitch measured from 20 to 60; a ceiling at or over it lets the field take it. - - The last four were earned by open edges alone, and the joint's cut taken again when - it comes out open (cutAgain in core.js) closes all four. Those ceilings stay where - they are until a sweep as fine as the one that found the small-pitch leaks says how - far each can go: the joint every 0.01 mm over its pitch band and every 0.05 of the - field past its ceiling, on both piece layouts, clean up to the new one. `waits` marks - such a step: it has to stay closed, or cutAgain has lost ground, and the field still - has to refuse it. */ + /* The snap from above and the puzzle's lobe are earned by their geometry, above. The + rest were earned by open edges — a puzzle at 19 and a bowtie at 15 at 0.35, a puzzle + key at 14 at 0.4 and at 42 at 0.82, the dovetail from 0.34 — and the joint's cut taken + again when it comes out open (cutAgain in core.js) closes every one. So each was swept + as connClrCeiling says: every 0.01 mm of pitch over its band and every 0.05 of the + field past it, on both piece layouts, and every 0.01 near anything that leaked. The + dovetail's step is open still, and is built with the other ends, above. Three more + have a leak within two steps, two cells' shells sharing an edge in the band they + overlap in, and the first is their row: it has to leak still, or that ceiling can go + up. The bowtie in the floor below 20 mm leaked nowhere up to 1.05 and takes the plain + 1 mm; `spare` marks its step to spare, which has to build closed, or cutAgain has lost + ground. Each has to be refused by the field. */ const PAST = [ ['snap from above, 0.35 at 42', { connector: 'snap', keyInsert: 'top', pitch: 42, clr: 0.35 }, '2x2 pieces'], ['puzzle, 0.3 at 13.5', { connector: 'puzzle', pitch: 13.5, clr: 0.3 }, '1-cell pieces'], - ['puzzle, 0.35 at 19', { connector: 'puzzle', pitch: 19, clr: 0.35 }, '2x2 pieces', 'waits'], - ['bowtie, 0.35 at 15', { connector: 'bowtie', pitch: 15, clr: 0.35 }, '2x2 pieces', 'waits'], - ['puzzle key, 0.4 at 14', { connector: 'puzzlekey', pitch: 14, clr: 0.4 }, '2x2 pieces', 'waits'], - ['puzzle key, 0.82 at 42', { connector: 'puzzlekey', pitch: 42, clr: 0.82 }, '2x2 pieces', 'waits'], + ['puzzle, 0.38 at 18.54', { connector: 'puzzle', pitch: 18.54, clr: 0.38 }, '1-cell pieces'], + ['puzzle key, 0.37 at 18.07', { connector: 'puzzlekey', pitch: 18.07, clr: 0.37 }, '1-cell pieces'], + ['puzzle key, 0.85 at 29.74', { connector: 'puzzlekey', pitch: 29.74, clr: 0.85 }, '1-cell pieces'], + ['bowtie, 1.05 at 18', { connector: 'bowtie', pitch: 18, clr: 1.05 }, '2x2 pieces', 'spare'], ]; - for (const [what, o, ln, waits] of PAST) { + for (const [what, o, ln, spare] of PAST) { const r = buildAll({ ...PIECE_LAYOUTS[ln](o.pitch), ...o }); /* A snap from above earns its ceiling in the seam face, before anything crosses the seam: one step past, its slot's wall is nearer the face than a BLOAT. */ @@ -2263,16 +2264,16 @@ console.log('\nthe fit clearance at its ceiling, every joint and pitch band:'); r.L.pieces[i]).keyed.map((st) => ({ ...st, clr: r.cfg.key.clr })), H)); } const inFace = face.sites > 0 && face.near < BLOAT - 1e-6; - const still = r.open > 0 || r.beyond > OVER || inFace; + const still = r.bad > 0 || r.beyond > OVER || inFace; const most = G.connClrCeiling(r.cfg).max, refused = o.clr > most + 1e-9; console.log(` ${what.padEnd(28)} ${r.beyond > OVER ? `${r.beyond} mm into the next piece` : inFace ? `its slot ${(Math.round(face.near * 1e4) / 1e4 + 0).toFixed(3)} mm off the seam face` : leakText(r)}` + (!refused ? ` THE FIELD TAKES IT: the ceiling went up to ${most}` - : waits ? (still ? ' OPEN AGAIN — cutAgain no longer closes it' - : ' — closed, and the ceiling waits on a sweep') + : spare ? (still ? ' OPEN AGAIN — cutAgain no longer closes it' + : ' — closed, a step to spare') : still ? ' — the ceiling is earned' : ' NOW CLEAN — that ceiling can go up')); - if (!refused || (waits ? still : !still)) bad++; + if (!refused || (spare ? still : !still)) bad++; } /* activeJoint in src/ui.js, with the field at the ceiling — a fixture, as in the coupon diff --git a/test/ui/plates-robustness.spec.js b/test/ui/plates-robustness.spec.js index 177de84..7e5a26e 100644 --- a/test/ui/plates-robustness.spec.js +++ b/test/ui/plates-robustness.spec.js @@ -353,7 +353,7 @@ test.describe('limits no tighter than the geometry', () => { // whose slot stands a BLOAT off the seam face [`${SEAM}&cn=snap&km=wall&ki=top&cl=0.3`, 0.25, 0.25], [`${SEAM}&cn=puzzlekey&cl=0.8`, 0.75, 0.75], - [`${seamAt(18)}&cn=bowtie&cl=0.3`, 0.25, 0.25], + [`${seamAt(18)}&cn=puzzlekey&cl=0.3`, 0.25, 0.25], [`${seamAt(13.5)}&cn=puzzle&cl=0.25`, 0.25, 0.25]]) test(`the fit sample stays inside the range: ${hash}`, async ({ page }) => { const errors = await openAt(page, hash); @@ -424,24 +424,25 @@ test.describe('limits no tighter than the geometry', () => { clip dropped in from above is housed in a slot whose seam-side wall stands 0.3 mm less the clearance from the seam, so at 0.35 on the field the wall lies in the seam face and past it in the next piece — the field stops at 0.3, a BLOAT short of the face; - and under 20 mm the puzzle, the bowtie and the puzzle key leave holes in the plate at + and under 20 mm the puzzle and the puzzle key leave plates that are not watertight at clearances that build closed at 42. Each case is refused at the field, clamped to the ceiling in the state a link loads into, and says which joint and which pitch — the pitch is the number to change. A joint that builds closed at a small pitch is not held - to the others' reason. */ + to the others' reason: the bowtie in the floor was, until a sweep built it closed from + 13.5 to 20 mm at every clearance to 1. */ test.describe('the clearance ceiling is the joint\'s and the pitch\'s', () => { const SEAM = '#w=168&d=84&sp=manual&rc=&cc=2'; for (const [hash, ceiling, msg] of [ [`${SEAM}&cn=snap&km=wall&ki=top&cl=0.35`, 0.3, /Fit clearance must be 0\.3 mm or less — any looser and the housing of a snap clip dropped in from above runs up to the seam and on into the next piece\./], - ['#pi=18&cn=bowtie&cl=1', 0.3, - /Fit clearance must be 0\.3 mm or less at an 18 mm pitch — on cells under 20 mm a looser bowtie key opens holes in the plate\./], + ['#pi=18&cn=puzzle&cl=1', 0.3, + /Fit clearance must be 0\.3 mm or less at an 18 mm pitch — on cells under 20 mm a looser puzzle tab leaves the plate not watertight\./], ['#pi=14&cn=puzzlekey&cl=0.5', 0.3, - /Fit clearance must be 0\.3 mm or less at a 14 mm pitch — on cells under 20 mm a looser puzzle key opens holes in the plate\./], + /Fit clearance must be 0\.3 mm or less at a 14 mm pitch — on cells under 20 mm a looser puzzle key leaves the plate not watertight\./], ['#pi=13.5&cn=puzzle&cl=0.3', 0.25, - /Fit clearance must be 0\.25 mm or less at a 13\.5 mm pitch — on cells under 20 mm a looser puzzle tab opens holes in the plate\./], + /Fit clearance must be 0\.25 mm or less at a 13\.5 mm pitch — on cells under 20 mm a looser puzzle tab leaves the plate not watertight\./], ['#cn=puzzlekey&cl=0.85', 0.8, - /Fit clearance must be 0\.8 mm or less — any looser and a puzzle key's recess opens holes in the plate\./], + /Fit clearance must be 0\.8 mm or less — any looser and a puzzle key's recess leaves the plate not watertight at some pitches\./], ]) test(`${hash} is held to ${ceiling}, and says why`, async ({ page }) => { const errors = await openAt(page, hash); @@ -457,9 +458,10 @@ test.describe('the clearance ceiling is the joint\'s and the pitch\'s', () => { expect(errors).toEqual([]); }); - // the snap and the H-clip built closed at every clearance and pitch measured + // the snap, the H-clip and the bowtie built closed at every clearance and pitch measured for (const hash of ['#pi=16&w=64&d=32&sp=manual&rc=&cc=2&cn=hclip&cl=1', - '#pi=16&w=64&d=32&sp=manual&rc=&cc=2&cn=snap&cl=1']) + '#pi=16&w=64&d=32&sp=manual&rc=&cc=2&cn=snap&cl=1', + '#pi=16&w=64&d=32&sp=manual&rc=&cc=2&cn=bowtie&cl=1']) test(`${hash} builds — the small-pitch hold is not every joint's`, async ({ page }) => { const errors = await openAt(page, hash); expect(await shown(page, 'errConnClr')).toBe(false); From 43cee557ec714588483dcac55e9fe41202f70b29 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 11:44:29 +0000 Subject: [PATCH 05/58] Rebuild the pages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 139 ++++++++++++++++++++++++++++--------------- index.html | 155 ++++++++++++++++++++++++++++++++---------------- sw.js | 2 +- 3 files changed, 196 insertions(+), 100 deletions(-) diff --git a/bins/index.html b/bins/index.html index 7e64f5e..a85366c 100644 --- a/bins/index.html +++ b/bins/index.html @@ -2419,6 +2419,36 @@

Your drawers

return polys; } +/* The polygon each mounting bore is cut as: how many sides, and how far out its corners + * stand for a size typed (`r`), and back the other way, the largest size whose corners + * stay inside a room (`fits`). fastenerCutter and screwCutter cut these and mountLimits + * holds the sizes to them, so a bore's shape is decided here and nowhere else. Each is + * cylinder's polygon about the site, a corner at every 2πk/sides from +x. + * + * Every bore stands its flats on the size asked, so its corners are 1/cos(π/sides) of the + * radius out and it is no narrower anywhere than the size typed. + * - magnet: 14 sides, cut 0.1 mm over the magnet's radius at its corners: 0.2 over the + * diameter, the press fit bins/bin.js keeps as well ("the plate's rule"). Its flats + * are cos(π/14) of that, and past a 7.78 mm magnet they came inside the magnet + * itself: 4.972 mm for a 10 mm one, a pocket narrower than what goes in it, where + * bins/bin.js's 16 sides keep their flats outside every magnet its field takes. So + * the corners go out as far as it takes for the flats to stand on the magnet's + * radius. For a magnet up to 7.77 mm, the 6 mm default among them, nothing moves. + * - head, the counterbore: 14 sides, and hole, the shank: 12. Both had their corners on + * the size asked, so their flats came inside it at every size, 5.85 mm across for the + * default 6 mm head and 2.90 for the 3 mm hole. Now their corners go out the same + * way, and no further: the magnet's 0.1 is a press fit, and a screw head is not + * pressed in. It drops into its counterbore, which the field already sizes with room + * round it (6 mm for an M3's 5.5 mm head), and the shank is a clearance hole. */ +const MOUNT_BORE = { + magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), + fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, + head: { sides: 14, r: (d) => d/2 / Math.cos(Math.PI/14), + fits: (room) => 2 * room * Math.cos(Math.PI / 14) }, + hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), + fits: (room) => 2 * room * Math.cos(Math.PI / 12) }, +}; + /* Everything removed at one mounting site, as ONE solid. * * Up to three cylinders share this axis: a magnet pocket, a screw shank, and the head's @@ -2440,22 +2470,17 @@

Your drawers

function fastenerCutter(cfg, magZ0, magZ1, shankTop) { let cut = null; const add = (c) => { cut = cut ? csgUnion(cut, c) : c; }; - /* A magnet's pocket is 14-sided, cut 0.1 mm over the magnet's radius at its corners: - 0.2 over the diameter, the press fit bins/bin.js keeps as well ("the plate's rule"). - Its flats are cos(π/14) of that, and past a 7.78 mm magnet they came inside the - magnet itself: 4.972 mm for a 10 mm one, a pocket narrower than what goes in it, - where bins/bin.js's 16 sides keep their flats outside every magnet its field takes. - So the corners go out as far as it takes for the flats to stand on the magnet's - radius. For a magnet up to 7.77 mm, the 6 mm default among them, nothing moves. */ - const magnetR = Math.max(cfg.magnetD/2 + 0.1, cfg.magnetD/2 / Math.cos(Math.PI/14)); + // each bore as MOUNT_BORE shapes it + const { magnet, head, hole } = MOUNT_BORE; + const magnetR = magnet.r(cfg.magnetD); if (cfg.magnets) add(cfg.magnetSide === 'top' - ? cylinder(0, 0, magnetR, magZ0, magZ1, 14) - : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, 14)); + ? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) + : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { if (cfg.screwHeadD > cfg.screwHoleD) - add(cylinder(0, 0, cfg.screwHeadD/2, -0.5, cfg.screwHeadDepth, 14)); - add(cylinder(0, 0, cfg.screwHoleD/2, -0.5, shankTop, 12)); + add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); + add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); } return cut; } @@ -2471,13 +2496,15 @@

Your drawers

})); } -// counterbore: hole cylinder full height + wider recess from chosen face +// counterbore: hole cylinder full height + wider recess from chosen face, each bore as +// MOUNT_BORE shapes it function screwCutter(cx, cy, holeD, headD, z0, z1, headDepth, fromTop) { - let polys = cylinder(cx, cy, holeD/2, z0 - 0.5, z1 + 0.5, 12); + const { head, hole } = MOUNT_BORE; + let polys = cylinder(cx, cy, hole.r(holeD), z0 - 0.5, z1 + 0.5, hole.sides); if (headD > holeD) { const rec = fromTop - ? cylinder(cx, cy, headD/2, z1 - headDepth, z1 + 0.5, 14) - : cylinder(cx, cy, headD/2, z0 - 0.5, z0 + headDepth, 14); + ? cylinder(cx, cy, head.r(headD), z1 - headDepth, z1 + 0.5, head.sides) + : cylinder(cx, cy, head.r(headD), z0 - 0.5, z0 + headDepth, head.sides); polys = csgUnion(polys, rec); } return polys; @@ -2600,13 +2627,23 @@

Your drawers

* for everything but the dovetail let a snap clip's housing into the next piece and let * small pitches leak. * - * Measured on 2 × 2 and 1-cell pieces from 13.5 to 60 mm, every 0.5 mm of pitch — at - * every 0.05 of clearance for the puzzle, the bowtie and the puzzle key, at six for the - * rest — and at every 0.01 of clearance at 20, 25, 30, 35, 42 and 60: + * Measured first on 2 × 2 and 1-cell pieces from 13.5 to 60 mm, every 0.5 mm of pitch — + * at every 0.05 of clearance for the puzzle, the bowtie and the puzzle key, at six for + * the rest — and at every 0.01 of clearance at 20, 25, 30, 35, 42 and 60. What leaked + * there was the sliver class, and buildPiece now cuts a joint again when its cut comes + * out open (cutAgain), so each ceiling was swept again on both layouts: every 0.01 mm of + * pitch over the band it governs (13.5 to 60 mm for the dovetail), every 0.05 of the + * field past it, and every 0.01 near anything that leaked. A ceiling goes up a step only + * where the field builds clean to the step after that one, which is then its step to + * spare; anything that leaks within two steps of a ceiling earns it. * - * dovetail 0.3. Past it the pocket, cut 1.9 mm + this into the piece, breaks through - * the 2.15 mm socket wall and crosses the chamfer cone; it leaked from 0.34. - * The hint's own advice stops at 0.25. + * dovetail 0.3, earned. The pocket, cut 1.9 mm + this into the piece, breaks through + * the 2.15 mm socket wall from 0.25 and crosses the chamfer cone. It leaked + * from 0.34, and cutAgain closes most of that, but one step past, 0.35 still + * leaves edges open at 42.35 mm on both layouts. Past that two cells' shells + * share an edge from 0.39, where the notch's wide end stands on the edge of + * the band they share (22.64 mm, and 22.65 at 0.4), and more leak or fold at + * 0.55, 0.75 and 0.8. The hint's own advice stops at 0.25. * snaptop 0.3, in either housing. The clip's slot puts its seam-side wall * 0.3 − (the key's clearance) from the seam, so past a field of 0.35 that * wall stands in the next piece — by 0.65 mm at 1 — and two pieces that print @@ -2617,15 +2654,23 @@

Your drawers

* pair did the same for any field from 0.3 up. 0.3 holds the wall one BLOAT, * 0.05, inside the piece, the overlap the cell regions keep; the coupon's * slackest pair is this ceiling cut the key's way, so it stops there too. - * pitch 0.3 below 20 mm, for the three housings the field cuts that leaked there: - * the puzzle, and the bowtie and puzzle key in the floor. Past 0.3 they left - * holes from 13.5 to 19 mm — a puzzle key at 14 from 0.4, a bowtie at 18 at - * 1, a puzzle at 19 from 0.35 — and from the default 0.2 to 0.3 none did. The - * puzzle's lobe also needs room in its cell: it opens the plate once its - * clearance passes half the pitch less 6.475 mm, so it stops at half the pitch - * less 6.5 — 0.25 at 13.5, and 0.3 from 13.6. - * joint 0.8 for a puzzle key in the floor from 20 mm up: between 0.82 and 0.96 its - * recess leaked at every pitch measured, 20 to 60. + * pitch 0.3 below 20 mm for the puzzle and a puzzle key in the floor, earned. Past + * it they left holes from 13.5 to 19 mm — a puzzle key at 14 from 0.4, a + * puzzle at 19 from 0.35 — which cutAgain closes, and 0.35 builds clean at + * every pitch; but on 1-cell pieces a line across the pitch that climbs with + * the field puts the cut on the edge of the band two cells share, and the + * shells share it: a puzzle key from 0.37 (at 18.07 mm), a puzzle tab from + * 0.38 (at 18.54). The puzzle's lobe also needs room in its cell: it opens the + * plate once its clearance passes half the pitch less 6.475 mm, so it stops at + * half the pitch less 6.5 — 0.25 at 13.5, and 0.3 from 13.6. A bowtie in the + * floor was held here too, for holes at 18 mm at 1. It builds clean at every + * 0.01 mm from 13.5 to 20 and every 0.05 up to 1.05, on both layouts, and + * loose and tight at every 0.1 mm, so it takes the plain 1 mm. + * joint 0.8 for a puzzle key in the floor from 20 mm up, earned. Its recess leaked + * between 0.82 and 0.96 at every pitch measured, 20 to 60, until cutAgain; + * at 0.85 it now stands on the edge of the band two cells share at 29.74 mm + * on 1-cell pieces, and at 0.86 at 29.79, though nowhere else from 20 to + * 51.7 at 0.85 or 0.9. * slip 1 for the rest. A key in the walls is cut to keySlim's own clearance, which * the field does not move. The snap and the H-clip did not leak above 0.3 at * any pitch measured but in one band — an H-clip dropped in from above leaks @@ -2634,19 +2679,21 @@

Your drawers

* 0.39–0.4 at 21 and 32.5–35 mm and here and there from 0.52 to 0.87. That is * the sliver class again, not a ceiling — no limit short of 0.38 misses them, * and the same class turns up under the default too: a snap at 20 mm at 0.29, - * a bowtie at 17 mm at 0.15 and under. + * a bowtie at 17 mm at 0.15 and under. One is not isolated: on 2 × 2 pieces + * a puzzle at 0.35 exactly, and not 0.34 or 0.36, has two shells sharing an + * edge at every pitch from 31.6 to 52.2 mm, where a corner of a notch's lobe, + * centred on the line between two cells, stands on the edge of the band they + * share. That wants mending in the cut; no ceiling misses a single value. * - * The leaks that set the pitch and joint ceilings, and the dovetail's at 0.34, were that - * class as well: buildPiece now cuts a joint again when its cut comes out open - * (cutAgain), and the step past each that test/plate-audit.js builds is closed, as is the - * H-clip from above at 0.74. The ceilings stay where they are until a sweep as fine as - * the one that found the small-pitch leaks says how far each can go. */ + * test/plate-audit.js builds the step that earns each ceiling, which has to leak or cross + * the seam and be refused, and the bowtie's step to spare, which has to build closed and + * be refused. */ function connClrCeiling(cfg) { const R = PLATE_RANGES.connClr; const kind = jointKind(cfg.connector, cfg.keyMount, cfg.keyInsert); if (kind === 'dovetail') return { max: R.dovetail, by: 'dovetail' }; if (kind === 'snaptop') return { max: R.snapTop, by: 'snaptop' }; - const floorKey = ['bowtie', 'puzzlekey'].includes(cfg.connector) && cfg.keyMount !== 'wall'; + const floorKey = cfg.connector === 'puzzlekey' && cfg.keyMount !== 'wall'; if (cfg.connector !== 'puzzle' && !floorKey) return { max: R.max, by: 'slip' }; if (cfg.pitch < R.smallPitch) { const room = cfg.connector === 'puzzle' @@ -4086,12 +4133,10 @@

Your drawers

const own = fit(cfg.magnetSide === 'top' ? top : under); const room = Math.min(own, magnetJoint); return { - // a magnet pocket's corners are 0.1 mm over the magnet's radius, or out to where its - // flats stand on that radius if that is further (fastenerCutter) - magnetD: r10(2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14))), - // a screw's bores have their corners on its size - screwHoleD: r10(2 * Math.min(fit(top), below)), - screwHeadD: r10(2 * Math.min(fit(under), below)), + // every room above is to a bore's corners, which stand out past its size (MOUNT_BORE) + magnetD: r10(MOUNT_BORE.magnet.fits(room)), + screwHoleD: r10(MOUNT_BORE.hole.fits(Math.min(fit(top), below))), + screwHeadD: r10(MOUNT_BORE.head.fits(Math.min(fit(under), below))), // in the solid floor the pad grows to suit, so only a boss caps the depth depth: bosses ? r10(BOSS_H - MOUNT_SKIN) : Infinity, // which of the sizes the cut beside it stops, rather than the floor, cell or boss @@ -4717,9 +4762,9 @@

Your drawers

* A 21.7 mm magnet from beneath at a 55 mm pitch did it: a flat of the pocket in the * cell at the back left, on the magnet's radius, crosses that cell's front side 0.03 * microns from the corner of the cell at the back right. A 22.2 mm screw shank at 56.5 - * mm did it twice over, from the cell at the back right as well, on main too. Each shell - * is closed on its own, so the pockets' own retries never see it, and it comes and goes - * with the size and the pitch. + * mm (22.2 across its corners, as shanks were cut then) did it twice over, from the cell + * at the back right as well, on main too. Each shell is closed on its own, so the + * pockets' own retries never see it, and it comes and goes with the size and the pitch. * * So with mounting pockets on the piece, every shell the cells and margins make is * checked against those built before it beside it, as jointed cells are above. One that diff --git a/index.html b/index.html index 5a940b7..04ab7c0 100644 --- a/index.html +++ b/index.html @@ -2206,6 +2206,36 @@

Your drawers

return polys; } +/* The polygon each mounting bore is cut as: how many sides, and how far out its corners + * stand for a size typed (`r`), and back the other way, the largest size whose corners + * stay inside a room (`fits`). fastenerCutter and screwCutter cut these and mountLimits + * holds the sizes to them, so a bore's shape is decided here and nowhere else. Each is + * cylinder's polygon about the site, a corner at every 2πk/sides from +x. + * + * Every bore stands its flats on the size asked, so its corners are 1/cos(π/sides) of the + * radius out and it is no narrower anywhere than the size typed. + * - magnet: 14 sides, cut 0.1 mm over the magnet's radius at its corners: 0.2 over the + * diameter, the press fit bins/bin.js keeps as well ("the plate's rule"). Its flats + * are cos(π/14) of that, and past a 7.78 mm magnet they came inside the magnet + * itself: 4.972 mm for a 10 mm one, a pocket narrower than what goes in it, where + * bins/bin.js's 16 sides keep their flats outside every magnet its field takes. So + * the corners go out as far as it takes for the flats to stand on the magnet's + * radius. For a magnet up to 7.77 mm, the 6 mm default among them, nothing moves. + * - head, the counterbore: 14 sides, and hole, the shank: 12. Both had their corners on + * the size asked, so their flats came inside it at every size, 5.85 mm across for the + * default 6 mm head and 2.90 for the 3 mm hole. Now their corners go out the same + * way, and no further: the magnet's 0.1 is a press fit, and a screw head is not + * pressed in. It drops into its counterbore, which the field already sizes with room + * round it (6 mm for an M3's 5.5 mm head), and the shank is a clearance hole. */ +const MOUNT_BORE = { + magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), + fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, + head: { sides: 14, r: (d) => d/2 / Math.cos(Math.PI/14), + fits: (room) => 2 * room * Math.cos(Math.PI / 14) }, + hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), + fits: (room) => 2 * room * Math.cos(Math.PI / 12) }, +}; + /* Everything removed at one mounting site, as ONE solid. * * Up to three cylinders share this axis: a magnet pocket, a screw shank, and the head's @@ -2227,22 +2257,17 @@

Your drawers

function fastenerCutter(cfg, magZ0, magZ1, shankTop) { let cut = null; const add = (c) => { cut = cut ? csgUnion(cut, c) : c; }; - /* A magnet's pocket is 14-sided, cut 0.1 mm over the magnet's radius at its corners: - 0.2 over the diameter, the press fit bins/bin.js keeps as well ("the plate's rule"). - Its flats are cos(π/14) of that, and past a 7.78 mm magnet they came inside the - magnet itself: 4.972 mm for a 10 mm one, a pocket narrower than what goes in it, - where bins/bin.js's 16 sides keep their flats outside every magnet its field takes. - So the corners go out as far as it takes for the flats to stand on the magnet's - radius. For a magnet up to 7.77 mm, the 6 mm default among them, nothing moves. */ - const magnetR = Math.max(cfg.magnetD/2 + 0.1, cfg.magnetD/2 / Math.cos(Math.PI/14)); + // each bore as MOUNT_BORE shapes it + const { magnet, head, hole } = MOUNT_BORE; + const magnetR = magnet.r(cfg.magnetD); if (cfg.magnets) add(cfg.magnetSide === 'top' - ? cylinder(0, 0, magnetR, magZ0, magZ1, 14) - : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, 14)); + ? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) + : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { if (cfg.screwHeadD > cfg.screwHoleD) - add(cylinder(0, 0, cfg.screwHeadD/2, -0.5, cfg.screwHeadDepth, 14)); - add(cylinder(0, 0, cfg.screwHoleD/2, -0.5, shankTop, 12)); + add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); + add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); } return cut; } @@ -2258,13 +2283,15 @@

Your drawers

})); } -// counterbore: hole cylinder full height + wider recess from chosen face +// counterbore: hole cylinder full height + wider recess from chosen face, each bore as +// MOUNT_BORE shapes it function screwCutter(cx, cy, holeD, headD, z0, z1, headDepth, fromTop) { - let polys = cylinder(cx, cy, holeD/2, z0 - 0.5, z1 + 0.5, 12); + const { head, hole } = MOUNT_BORE; + let polys = cylinder(cx, cy, hole.r(holeD), z0 - 0.5, z1 + 0.5, hole.sides); if (headD > holeD) { const rec = fromTop - ? cylinder(cx, cy, headD/2, z1 - headDepth, z1 + 0.5, 14) - : cylinder(cx, cy, headD/2, z0 - 0.5, z0 + headDepth, 14); + ? cylinder(cx, cy, head.r(headD), z1 - headDepth, z1 + 0.5, head.sides) + : cylinder(cx, cy, head.r(headD), z0 - 0.5, z0 + headDepth, head.sides); polys = csgUnion(polys, rec); } return polys; @@ -2387,13 +2414,23 @@

Your drawers

* for everything but the dovetail let a snap clip's housing into the next piece and let * small pitches leak. * - * Measured on 2 × 2 and 1-cell pieces from 13.5 to 60 mm, every 0.5 mm of pitch — at - * every 0.05 of clearance for the puzzle, the bowtie and the puzzle key, at six for the - * rest — and at every 0.01 of clearance at 20, 25, 30, 35, 42 and 60: + * Measured first on 2 × 2 and 1-cell pieces from 13.5 to 60 mm, every 0.5 mm of pitch — + * at every 0.05 of clearance for the puzzle, the bowtie and the puzzle key, at six for + * the rest — and at every 0.01 of clearance at 20, 25, 30, 35, 42 and 60. What leaked + * there was the sliver class, and buildPiece now cuts a joint again when its cut comes + * out open (cutAgain), so each ceiling was swept again on both layouts: every 0.01 mm of + * pitch over the band it governs (13.5 to 60 mm for the dovetail), every 0.05 of the + * field past it, and every 0.01 near anything that leaked. A ceiling goes up a step only + * where the field builds clean to the step after that one, which is then its step to + * spare; anything that leaks within two steps of a ceiling earns it. * - * dovetail 0.3. Past it the pocket, cut 1.9 mm + this into the piece, breaks through - * the 2.15 mm socket wall and crosses the chamfer cone; it leaked from 0.34. - * The hint's own advice stops at 0.25. + * dovetail 0.3, earned. The pocket, cut 1.9 mm + this into the piece, breaks through + * the 2.15 mm socket wall from 0.25 and crosses the chamfer cone. It leaked + * from 0.34, and cutAgain closes most of that, but one step past, 0.35 still + * leaves edges open at 42.35 mm on both layouts. Past that two cells' shells + * share an edge from 0.39, where the notch's wide end stands on the edge of + * the band they share (22.64 mm, and 22.65 at 0.4), and more leak or fold at + * 0.55, 0.75 and 0.8. The hint's own advice stops at 0.25. * snaptop 0.3, in either housing. The clip's slot puts its seam-side wall * 0.3 − (the key's clearance) from the seam, so past a field of 0.35 that * wall stands in the next piece — by 0.65 mm at 1 — and two pieces that print @@ -2404,15 +2441,23 @@

Your drawers

* pair did the same for any field from 0.3 up. 0.3 holds the wall one BLOAT, * 0.05, inside the piece, the overlap the cell regions keep; the coupon's * slackest pair is this ceiling cut the key's way, so it stops there too. - * pitch 0.3 below 20 mm, for the three housings the field cuts that leaked there: - * the puzzle, and the bowtie and puzzle key in the floor. Past 0.3 they left - * holes from 13.5 to 19 mm — a puzzle key at 14 from 0.4, a bowtie at 18 at - * 1, a puzzle at 19 from 0.35 — and from the default 0.2 to 0.3 none did. The - * puzzle's lobe also needs room in its cell: it opens the plate once its - * clearance passes half the pitch less 6.475 mm, so it stops at half the pitch - * less 6.5 — 0.25 at 13.5, and 0.3 from 13.6. - * joint 0.8 for a puzzle key in the floor from 20 mm up: between 0.82 and 0.96 its - * recess leaked at every pitch measured, 20 to 60. + * pitch 0.3 below 20 mm for the puzzle and a puzzle key in the floor, earned. Past + * it they left holes from 13.5 to 19 mm — a puzzle key at 14 from 0.4, a + * puzzle at 19 from 0.35 — which cutAgain closes, and 0.35 builds clean at + * every pitch; but on 1-cell pieces a line across the pitch that climbs with + * the field puts the cut on the edge of the band two cells share, and the + * shells share it: a puzzle key from 0.37 (at 18.07 mm), a puzzle tab from + * 0.38 (at 18.54). The puzzle's lobe also needs room in its cell: it opens the + * plate once its clearance passes half the pitch less 6.475 mm, so it stops at + * half the pitch less 6.5 — 0.25 at 13.5, and 0.3 from 13.6. A bowtie in the + * floor was held here too, for holes at 18 mm at 1. It builds clean at every + * 0.01 mm from 13.5 to 20 and every 0.05 up to 1.05, on both layouts, and + * loose and tight at every 0.1 mm, so it takes the plain 1 mm. + * joint 0.8 for a puzzle key in the floor from 20 mm up, earned. Its recess leaked + * between 0.82 and 0.96 at every pitch measured, 20 to 60, until cutAgain; + * at 0.85 it now stands on the edge of the band two cells share at 29.74 mm + * on 1-cell pieces, and at 0.86 at 29.79, though nowhere else from 20 to + * 51.7 at 0.85 or 0.9. * slip 1 for the rest. A key in the walls is cut to keySlim's own clearance, which * the field does not move. The snap and the H-clip did not leak above 0.3 at * any pitch measured but in one band — an H-clip dropped in from above leaks @@ -2421,19 +2466,21 @@

Your drawers

* 0.39–0.4 at 21 and 32.5–35 mm and here and there from 0.52 to 0.87. That is * the sliver class again, not a ceiling — no limit short of 0.38 misses them, * and the same class turns up under the default too: a snap at 20 mm at 0.29, - * a bowtie at 17 mm at 0.15 and under. + * a bowtie at 17 mm at 0.15 and under. One is not isolated: on 2 × 2 pieces + * a puzzle at 0.35 exactly, and not 0.34 or 0.36, has two shells sharing an + * edge at every pitch from 31.6 to 52.2 mm, where a corner of a notch's lobe, + * centred on the line between two cells, stands on the edge of the band they + * share. That wants mending in the cut; no ceiling misses a single value. * - * The leaks that set the pitch and joint ceilings, and the dovetail's at 0.34, were that - * class as well: buildPiece now cuts a joint again when its cut comes out open - * (cutAgain), and the step past each that test/plate-audit.js builds is closed, as is the - * H-clip from above at 0.74. The ceilings stay where they are until a sweep as fine as - * the one that found the small-pitch leaks says how far each can go. */ + * test/plate-audit.js builds the step that earns each ceiling, which has to leak or cross + * the seam and be refused, and the bowtie's step to spare, which has to build closed and + * be refused. */ function connClrCeiling(cfg) { const R = PLATE_RANGES.connClr; const kind = jointKind(cfg.connector, cfg.keyMount, cfg.keyInsert); if (kind === 'dovetail') return { max: R.dovetail, by: 'dovetail' }; if (kind === 'snaptop') return { max: R.snapTop, by: 'snaptop' }; - const floorKey = ['bowtie', 'puzzlekey'].includes(cfg.connector) && cfg.keyMount !== 'wall'; + const floorKey = cfg.connector === 'puzzlekey' && cfg.keyMount !== 'wall'; if (cfg.connector !== 'puzzle' && !floorKey) return { max: R.max, by: 'slip' }; if (cfg.pitch < R.smallPitch) { const room = cfg.connector === 'puzzle' @@ -3873,12 +3920,10 @@

Your drawers

const own = fit(cfg.magnetSide === 'top' ? top : under); const room = Math.min(own, magnetJoint); return { - // a magnet pocket's corners are 0.1 mm over the magnet's radius, or out to where its - // flats stand on that radius if that is further (fastenerCutter) - magnetD: r10(2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14))), - // a screw's bores have their corners on its size - screwHoleD: r10(2 * Math.min(fit(top), below)), - screwHeadD: r10(2 * Math.min(fit(under), below)), + // every room above is to a bore's corners, which stand out past its size (MOUNT_BORE) + magnetD: r10(MOUNT_BORE.magnet.fits(room)), + screwHoleD: r10(MOUNT_BORE.hole.fits(Math.min(fit(top), below))), + screwHeadD: r10(MOUNT_BORE.head.fits(Math.min(fit(under), below))), // in the solid floor the pad grows to suit, so only a boss caps the depth depth: bosses ? r10(BOSS_H - MOUNT_SKIN) : Infinity, // which of the sizes the cut beside it stops, rather than the floor, cell or boss @@ -4504,9 +4549,9 @@

Your drawers

* A 21.7 mm magnet from beneath at a 55 mm pitch did it: a flat of the pocket in the * cell at the back left, on the magnet's radius, crosses that cell's front side 0.03 * microns from the corner of the cell at the back right. A 22.2 mm screw shank at 56.5 - * mm did it twice over, from the cell at the back right as well, on main too. Each shell - * is closed on its own, so the pockets' own retries never see it, and it comes and goes - * with the size and the pitch. + * mm (22.2 across its corners, as shanks were cut then) did it twice over, from the cell + * at the back right as well, on main too. Each shell is closed on its own, so the + * pockets' own retries never see it, and it comes and goes with the size and the pitch. * * So with mounting pockets on the piece, every shell the cells and margins make is * checked against those built before it beside it, as jointed cells are above. One that @@ -7464,15 +7509,21 @@

Your drawers

/* What sets the clearance's ceiling, in words: core.js connClrCeiling decides it and says which reason applies. A ceiling that moves with the pitch names the pitch, as the mount sizes do, because that is the number to change; 'slip' is the plain 1 mm and keeps the - millimetres advice. */ -const CLR_JOINT = { puzzle: 'puzzle tab', bowtie: 'bowtie key', puzzlekey: 'puzzle key' }; + millimetres advice. The puzzle and the puzzle key said they opened holes in the plate, + as they did until a joint's cut was taken again when it came out open (cutAgain in + core.js); past each ceiling now two cells' shells share an edge at some pitches, which + is not watertight either but is no hole. The dovetail said its pocket broke through + into the socket beside it, which it does from 0.25, under its ceiling. */ +const CLR_JOINT = { puzzle: 'puzzle tab', puzzlekey: 'puzzle key' }; const clrWhy = (by) => ({ - dovetail: '— any looser and a dovetail pocket breaks through into the socket beside it', + dovetail: '— any looser and a dovetail pocket leaves the plate not watertight at some ' + + 'pitches', snaptop: '— any looser and the housing of a snap clip dropped in from above runs up to ' + 'the seam and on into the next piece', pitch: `${atPitch()} — on cells under ${RANGES.connClr.smallPitch} mm a ` + - `looser ${CLR_JOINT[state.connector]} opens holes in the plate`, - joint: `— any looser and a ${CLR_JOINT[state.connector]}'s recess opens holes in the plate`, + `looser ${CLR_JOINT[state.connector]} leaves the plate not watertight`, + joint: `— any looser and a ${CLR_JOINT[state.connector]}'s recess leaves the plate not ` + + 'watertight at some pitches', })[by] || ''; /* id -> the message that goes under it. Rebuilt from scratch on every read, so a field that has come good stops complaining without anything having to remember it once did. */ diff --git a/sw.js b/sw.js index 827cb0e..c43f938 100644 --- a/sw.js +++ b/sw.js @@ -52,7 +52,7 @@ load from this site, and the app's icons and manifest, as paths from the root. VERSION is a hash of all of those files, names and bytes, and of this script as it is served, FILES filled in and everything else but VERSION itself. */ -const VERSION = "508e30fe00d3"; +const VERSION = "9ddeb0107071"; const FILES = [ "./", "bins/", From 69e610d8eaaf3b1b083df17d9b35399ea2552e7d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 12:19:23 +0000 Subject: [PATCH 06/58] Keep puzzle plates watertight on pitches just past 20 mm by holding the fit clearance at 0.3 up to 20.7 mm Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 33 ++++++++-------- src/core.js | 63 +++++++++++++++++-------------- src/ui.js | 19 +++++----- test/plate-audit.js | 23 +++++++---- test/ui/plates-robustness.spec.js | 18 +++++---- 5 files changed, 88 insertions(+), 68 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 2e543cb..a0c478a 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -825,21 +825,24 @@ currently installed): meet face to face at 14.44 mm (twice a cup's reach), and there they are built as one solid with `csgUnion`. - **None of the fit clearance's ceilings that waited on a sweep can go up but the - bowtie's.** `connClrCeiling` caps the field for each joint, and five of its ceilings - were set by open edges one step past them that `cutAgain` closes. Swept again on 2 × 2 - and 1-cell pieces, every 0.01 mm of pitch over each ceiling's band, every 0.05 of the - field past it and every 0.01 near a leak: the dovetail's step past its 0.3 is open - still at one pitch (0.35 at 42.35 mm), and the puzzle key in the floor's past its 0.8 - leaks at another (0.85 at 29.74). The puzzle and the puzzle key under 20 mm leak within - two steps of their 0.3 (0.38 at 18.54 mm, 0.37 at 18.07). The puzzle key's and the - puzzle's are no longer holes: the joint's cut lands on the edge of the `BLOAT` band two - cells overlap in, along a line that climbs with the field, and the two shells share - that edge, as the dovetail's do from 0.39. A ceiling goes up a step only where the - field builds clean to the step after it, so all four stay. The bowtie in the floor - under 20 mm leaked nowhere up to 1.05, at every 0.01 mm from 13.5 to 20 and loose and - tight at every 0.1, and takes the plain 1 mm. `test/plate-audit.js` builds each leak, - which has to stay one and be refused, and the bowtie's step to spare, which has to - build closed. + bowtie's, and the puzzle's runs on to 20.7 mm.** `connClrCeiling` caps the field for + each joint, and five of its ceilings were set by open edges one step past them that + `cutAgain` closes. Swept again on 2 × 2 and 1-cell pieces, every 0.01 mm of pitch over + each ceiling's band, every 0.05 of the field past it and every 0.01 near a leak: the + dovetail's step past its 0.3 is open still at one pitch (0.35 at 42.35 mm), and the + puzzle key in the floor's past its 0.8 leaks at another (0.85 at 29.74). The puzzle and + the puzzle key under 20 mm leak within two steps of their 0.3 (0.38 at 18.54 mm, 0.37 at + 18.07). The puzzle key's and the puzzle's are no longer holes: the joint's cut lands on + the edge of the `BLOAT` band two cells overlap in, along a line that climbs with the + field, and the two shells share that edge, as the dovetail's do from 0.39. The puzzle's + line runs on past 20 mm, 0.05 higher every 0.16 mm (0.85 at 20.05, 0.9 at 20.21, 1 at + 20.53, 1.05 at 20.69), so its 0.3 holds on to 20.7, where the plain 1 mm has its step to + spare. A ceiling goes up a step only where the field builds clean to the step after it, + so all four stay. The bowtie in the floor under 20 mm leaked nowhere up to 1.05, at + every 0.01 mm from 13.5 to 20 and loose and tight at every 0.1, and takes the plain + 1 mm. `test/plate-audit.js` builds each leak, which has to stay one and be refused, and + the bowtie's step to spare and the step under the puzzle's line at 20.05 mm, which have + to build closed. - **A face that is nothing but a straight line has no middle.** A weld that closes a T-junction can leave three corners on one line. Fanned from its average, the spokes land a fraction of a micron from a corner and read, to `checkManifold`'s thousandths, as the diff --git a/src/core.js b/src/core.js index 3e9e3be..2602c9f 100644 --- a/src/core.js +++ b/src/core.js @@ -999,7 +999,7 @@ const PLATE_RANGES = { pitch: { min: 13.5, max: 200 }, topCutoff: { min: 0.1, max: 1 }, connClr: { min: 0, max: 1, dovetail: 0.3, snapTop: 0.3, puzzleKey: 0.8, - smallPitch: 20, small: 0.3, puzzleRoom: 6.5 }, + smallPitch: 20, puzzlePitch: 20.7, small: 0.3, puzzleRoom: 6.5 }, bottomPad: { min: 0, max: 20 }, magnetD: { min: 1 }, magnetH: { min: 0.5, max: 10 }, screwHoleD: { min: 1 }, screwHeadD: { min: 1 }, screwHeadDepth: { min: 0.5, max: 10 }, @@ -1014,12 +1014,12 @@ function fitClearances(field) { } /* The fit clearance's ceiling for one design, as { max, by }; `by` says what sets it, for * the page to put in words: 'dovetail', 'snaptop', 'pitch', 'joint', or 'slip' for the - * plain 1 mm. One function, because four things have to agree on it — the field's range, - * the clamp on a number a link brings in, the message saying why, and the fit coupon's - * slackest pair — and it takes the whole design because the answer depends on the joint, - * which way its key goes in and the pitch. It took a connector name, and a 1 mm ceiling - * for everything but the dovetail let a snap clip's housing into the next piece and let - * small pitches leak. + * plain 1 mm, and with 'pitch', `below` is the pitch its band runs up to. One function, + * because four things have to agree on it — the field's range, the clamp on a number a + * link brings in, the message saying why, and the fit coupon's slackest pair — and it + * takes the whole design because the answer depends on the joint, which way its key goes + * in and the pitch. It took a connector name, and a 1 mm ceiling for everything but the + * dovetail let a snap clip's housing into the next piece and let small pitches leak. * * Measured first on 2 × 2 and 1-cell pieces from 13.5 to 60 mm, every 0.5 mm of pitch — * at every 0.05 of clearance for the puzzle, the bowtie and the puzzle key, at six for @@ -1048,13 +1048,18 @@ function fitClearances(field) { * pair did the same for any field from 0.3 up. 0.3 holds the wall one BLOAT, * 0.05, inside the piece, the overlap the cell regions keep; the coupon's * slackest pair is this ceiling cut the key's way, so it stops there too. - * pitch 0.3 below 20 mm for the puzzle and a puzzle key in the floor, earned. Past - * it they left holes from 13.5 to 19 mm — a puzzle key at 14 from 0.4, a - * puzzle at 19 from 0.35 — which cutAgain closes, and 0.35 builds clean at - * every pitch; but on 1-cell pieces a line across the pitch that climbs with - * the field puts the cut on the edge of the band two cells share, and the - * shells share it: a puzzle key from 0.37 (at 18.07 mm), a puzzle tab from - * 0.38 (at 18.54). The puzzle's lobe also needs room in its cell: it opens the + * pitch 0.3 for a puzzle below 20.7 mm and a puzzle key in the floor below 20, + * earned. Past it they left holes from 13.5 to 19 mm — a puzzle key at 14 + * from 0.4, a puzzle at 19 from 0.35 — which cutAgain closes, and 0.35 builds + * clean at every pitch; but on 1-cell pieces a line across the pitch that + * climbs with the field puts the cut on the edge of the band two cells share, + * and the shells share it: a puzzle key from 0.37 (at 18.07 mm), a puzzle tab + * from 0.38 (at 18.54). The puzzle's line climbs 0.05 every 0.16 mm and runs + * on past 20 — 0.8 at 19.89, 0.85 at 20.05, 0.9 at 20.21 — out of the field, + * 1 at 20.53 and 1.05 at 20.69, so its band runs to 20.7, where the plain + * 1 mm has its step to spare. Off the line the field builds clean from 20 to + * 20.7, and a ceiling that followed the line would gain only there; one band + * is simpler. The puzzle's lobe also needs room in its cell: it opens the * plate once its clearance passes half the pitch less 6.475 mm, so it stops at * half the pitch less 6.5 — 0.25 at 13.5, and 0.3 from 13.6. A bowtie in the * floor was held here too, for holes at 18 mm at 1. It builds clean at every @@ -1068,20 +1073,21 @@ function fitClearances(field) { * slip 1 for the rest. A key in the walls is cut to keySlim's own clearance, which * the field does not move. The snap and the H-clip did not leak above 0.3 at * any pitch measured but in one band — an H-clip dropped in from above leaks - * at 0.74 to 0.745 and nowhere else. From 20 mm up the bowtie and the puzzle - * leak only at isolated values: the bowtie at 25 mm at 0.64, the puzzle at - * 0.39–0.4 at 21 and 32.5–35 mm and here and there from 0.52 to 0.87. That is - * the sliver class again, not a ceiling — no limit short of 0.38 misses them, - * and the same class turns up under the default too: a snap at 20 mm at 0.29, - * a bowtie at 17 mm at 0.15 and under. One is not isolated: on 2 × 2 pieces - * a puzzle at 0.35 exactly, and not 0.34 or 0.36, has two shells sharing an - * edge at every pitch from 31.6 to 52.2 mm, where a corner of a notch's lobe, - * centred on the line between two cells, stands on the edge of the band they - * share. That wants mending in the cut; no ceiling misses a single value. + * at 0.74 to 0.745 and nowhere else. From 20 mm up (20.7 for the puzzle) the + * bowtie and the puzzle leak only at isolated values: the bowtie at 25 mm at + * 0.64, the puzzle at 0.39–0.4 at 21 and 32.5–35 mm and here and there from + * 0.52 to 0.87. That is the sliver class again, not a ceiling — no limit + * short of 0.38 misses them, and the same class turns up under the default + * too: a snap at 20 mm at 0.29, a bowtie at 17 mm at 0.15 and under. One is + * not isolated: on 2 × 2 pieces a puzzle at 0.35 exactly, and not 0.34 or + * 0.36, has two shells sharing an edge at every pitch from 31.6 to 52.2 mm, + * where a corner of a notch's lobe, centred on the line between two cells, + * stands on the edge of the band they share. That wants mending in the cut + * (issue #72); no ceiling misses a single value. * * test/plate-audit.js builds the step that earns each ceiling, which has to leak or cross - * the seam and be refused, and the bowtie's step to spare, which has to build closed and - * be refused. */ + * the seam and be refused, and the bowtie's step to spare and the step under the puzzle's + * line past 20 mm, which have to build closed and be refused. */ function connClrCeiling(cfg) { const R = PLATE_RANGES.connClr; const kind = jointKind(cfg.connector, cfg.keyMount, cfg.keyInsert); @@ -1089,10 +1095,11 @@ function connClrCeiling(cfg) { if (kind === 'snaptop') return { max: R.snapTop, by: 'snaptop' }; const floorKey = cfg.connector === 'puzzlekey' && cfg.keyMount !== 'wall'; if (cfg.connector !== 'puzzle' && !floorKey) return { max: R.max, by: 'slip' }; - if (cfg.pitch < R.smallPitch) { + const below = cfg.connector === 'puzzle' ? R.puzzlePitch : R.smallPitch; + if (cfg.pitch < below) { const room = cfg.connector === 'puzzle' ? Math.floor((cfg.pitch / 2 - R.puzzleRoom) * 100 + 1e-9) / 100 : Infinity; - return { max: Math.min(R.small, room), by: 'pitch' }; + return { max: Math.min(R.small, room), by: 'pitch', below }; } if (cfg.connector === 'puzzlekey') return { max: R.puzzleKey, by: 'joint' }; return { max: R.max, by: 'slip' }; diff --git a/src/ui.js b/src/ui.js index 41c5793..b951c94 100644 --- a/src/ui.js +++ b/src/ui.js @@ -135,23 +135,24 @@ const LIMITS = { label: 'Screw head depth', when: () => state.screws, why: bossDepth }, // each joint's ceiling is its own, so none is held to another's reason; see clrWhy connClr: { min: RANGES.connClr.min, max: () => connClrCeiling(state).max, label: 'Fit clearance', - when: () => state.connector !== 'none', why: () => clrWhy(connClrCeiling(state).by) }, + when: () => state.connector !== 'none', why: () => clrWhy(connClrCeiling(state)) }, }; /* What sets the clearance's ceiling, in words: core.js connClrCeiling decides it and says which reason applies. A ceiling that moves with the pitch names the pitch, as the mount - sizes do, because that is the number to change; 'slip' is the plain 1 mm and keeps the - millimetres advice. The puzzle and the puzzle key said they opened holes in the plate, - as they did until a joint's cut was taken again when it came out open (cutAgain in - core.js); past each ceiling now two cells' shells share an edge at some pitches, which - is not watertight either but is no hole. The dovetail said its pocket broke through - into the socket beside it, which it does from 0.25, under its ceiling. */ + sizes do, because that is the number to change, and the pitch its band runs up to, + which for the puzzle is its own; 'slip' is the plain 1 mm and keeps the millimetres + advice. The puzzle and the puzzle key said they opened holes in the plate, as they did + until a joint's cut was taken again when it came out open (cutAgain in core.js); past + each ceiling now two cells' shells share an edge at some pitches, which is not + watertight either but is no hole. The dovetail said its pocket broke through into the + socket beside it, which it does from 0.25, under its ceiling. */ const CLR_JOINT = { puzzle: 'puzzle tab', puzzlekey: 'puzzle key' }; -const clrWhy = (by) => ({ +const clrWhy = ({ by, below }) => ({ dovetail: '— any looser and a dovetail pocket leaves the plate not watertight at some ' + 'pitches', snaptop: '— any looser and the housing of a snap clip dropped in from above runs up to ' + 'the seam and on into the next piece', - pitch: `${atPitch()} — on cells under ${RANGES.connClr.smallPitch} mm a ` + + pitch: `${atPitch()} — on cells under ${below} mm a ` + `looser ${CLR_JOINT[state.connector]} leaves the plate not watertight`, joint: `— any looser and a ${CLR_JOINT[state.connector]}'s recess leaves the plate not ` + 'watertight at some pitches', diff --git a/test/plate-audit.js b/test/plate-audit.js index b1d3e52..78d9fb8 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -2242,18 +2242,26 @@ console.log('\nthe fit clearance at its ceiling, every joint and pitch band:'); dovetail's step is open still, and is built with the other ends, above. Three more have a leak within two steps, two cells' shells sharing an edge in the band they overlap in, and the first is their row: it has to leak still, or that ceiling can go - up. The bowtie in the floor below 20 mm leaked nowhere up to 1.05 and takes the plain - 1 mm; `spare` marks its step to spare, which has to build closed, or cutAgain has lost - ground. Each has to be refused by the field. */ + up. The puzzle's leak runs on past 20 mm as a line, and its band with it to 20.7. + The line's first leak past 20 is a row, with the step under it, which has to build + closed so the row stays on the line; so is the field's own 1 mm on the line, which + the band has to reach past. The bowtie in the floor below 20 mm leaked nowhere up to + 1.05 and takes the plain 1 mm; its step to spare has to build closed, or cutAgain + has lost ground. A row that has to build closed says why. Each has to be refused by + the field. */ const PAST = [ ['snap from above, 0.35 at 42', { connector: 'snap', keyInsert: 'top', pitch: 42, clr: 0.35 }, '2x2 pieces'], ['puzzle, 0.3 at 13.5', { connector: 'puzzle', pitch: 13.5, clr: 0.3 }, '1-cell pieces'], ['puzzle, 0.38 at 18.54', { connector: 'puzzle', pitch: 18.54, clr: 0.38 }, '1-cell pieces'], + ['puzzle, 0.85 at 20.05', { connector: 'puzzle', pitch: 20.05, clr: 0.85 }, '1-cell pieces'], + ['puzzle, 0.8 at 20.05', { connector: 'puzzle', pitch: 20.05, clr: 0.8 }, '1-cell pieces', + 'the step under the line'], + ['puzzle, 1 at 20.53', { connector: 'puzzle', pitch: 20.53, clr: 1 }, '1-cell pieces'], ['puzzle key, 0.37 at 18.07', { connector: 'puzzlekey', pitch: 18.07, clr: 0.37 }, '1-cell pieces'], ['puzzle key, 0.85 at 29.74', { connector: 'puzzlekey', pitch: 29.74, clr: 0.85 }, '1-cell pieces'], - ['bowtie, 1.05 at 18', { connector: 'bowtie', pitch: 18, clr: 1.05 }, '2x2 pieces', 'spare'], + ['bowtie, 1.05 at 18', { connector: 'bowtie', pitch: 18, clr: 1.05 }, '2x2 pieces', 'a step to spare'], ]; - for (const [what, o, ln, spare] of PAST) { + for (const [what, o, ln, closed] of PAST) { const r = buildAll({ ...PIECE_LAYOUTS[ln](o.pitch), ...o }); /* A snap from above earns its ceiling in the seam face, before anything crosses the seam: one step past, its slot's wall is nearer the face than a BLOAT. */ @@ -2270,10 +2278,9 @@ console.log('\nthe fit clearance at its ceiling, every joint and pitch band:'); : inFace ? `its slot ${(Math.round(face.near * 1e4) / 1e4 + 0).toFixed(3)} mm off the seam face` : leakText(r)}` + (!refused ? ` THE FIELD TAKES IT: the ceiling went up to ${most}` - : spare ? (still ? ' OPEN AGAIN — cutAgain no longer closes it' - : ' — closed, a step to spare') + : closed ? (still ? ` OPEN — no longer ${closed}` : ` — closed, ${closed}`) : still ? ' — the ceiling is earned' : ' NOW CLEAN — that ceiling can go up')); - if (!refused || (spare ? still : !still)) bad++; + if (!refused || (closed ? still : !still)) bad++; } /* activeJoint in src/ui.js, with the field at the ceiling — a fixture, as in the coupon diff --git a/test/ui/plates-robustness.spec.js b/test/ui/plates-robustness.spec.js index 7e5a26e..e86b832 100644 --- a/test/ui/plates-robustness.spec.js +++ b/test/ui/plates-robustness.spec.js @@ -424,23 +424,25 @@ test.describe('limits no tighter than the geometry', () => { clip dropped in from above is housed in a slot whose seam-side wall stands 0.3 mm less the clearance from the seam, so at 0.35 on the field the wall lies in the seam face and past it in the next piece — the field stops at 0.3, a BLOAT short of the face; - and under 20 mm the puzzle and the puzzle key leave plates that are not watertight at - clearances that build closed at 42. Each case is refused at the field, clamped to the - ceiling in the state a link loads into, and says which joint and which pitch — the - pitch is the number to change. A joint that builds closed at a small pitch is not held - to the others' reason: the bowtie in the floor was, until a sweep built it closed from - 13.5 to 20 mm at every clearance to 1. */ + and under 20 mm (20.7 for the puzzle) the puzzle and the puzzle key leave plates that + are not watertight at clearances that build closed at 42. Each case is refused at the + field, clamped to the ceiling in the state a link loads into, and says which joint and + which pitch — the pitch is the number to change. A joint that builds closed at a small + pitch is not held to the others' reason: the bowtie in the floor was, until a sweep + built it closed from 13.5 to 20 mm at every clearance to 1. */ test.describe('the clearance ceiling is the joint\'s and the pitch\'s', () => { const SEAM = '#w=168&d=84&sp=manual&rc=&cc=2'; for (const [hash, ceiling, msg] of [ [`${SEAM}&cn=snap&km=wall&ki=top&cl=0.35`, 0.3, /Fit clearance must be 0\.3 mm or less — any looser and the housing of a snap clip dropped in from above runs up to the seam and on into the next piece\./], ['#pi=18&cn=puzzle&cl=1', 0.3, - /Fit clearance must be 0\.3 mm or less at an 18 mm pitch — on cells under 20 mm a looser puzzle tab leaves the plate not watertight\./], + /Fit clearance must be 0\.3 mm or less at an 18 mm pitch — on cells under 20\.7 mm a looser puzzle tab leaves the plate not watertight\./], ['#pi=14&cn=puzzlekey&cl=0.5', 0.3, /Fit clearance must be 0\.3 mm or less at a 14 mm pitch — on cells under 20 mm a looser puzzle key leaves the plate not watertight\./], ['#pi=13.5&cn=puzzle&cl=0.3', 0.25, - /Fit clearance must be 0\.25 mm or less at a 13\.5 mm pitch — on cells under 20 mm a looser puzzle tab leaves the plate not watertight\./], + /Fit clearance must be 0\.25 mm or less at a 13\.5 mm pitch — on cells under 20\.7 mm a looser puzzle tab leaves the plate not watertight\./], + ['#pi=20.3&cn=puzzle&cl=0.9', 0.3, + /Fit clearance must be 0\.3 mm or less at a 20\.3 mm pitch — on cells under 20\.7 mm a looser puzzle tab leaves the plate not watertight\./], ['#cn=puzzlekey&cl=0.85', 0.8, /Fit clearance must be 0\.8 mm or less — any looser and a puzzle key's recess leaves the plate not watertight at some pitches\./], ]) From 07fe871469f3caffd6a630b1a4aab948877ede62 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 12:19:31 +0000 Subject: [PATCH 07/58] Rebuild the pages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 63 ++++++++++++++++++++----------------- index.html | 82 +++++++++++++++++++++++++++---------------------- sw.js | 2 +- 3 files changed, 81 insertions(+), 66 deletions(-) diff --git a/bins/index.html b/bins/index.html index a85366c..6119e71 100644 --- a/bins/index.html +++ b/bins/index.html @@ -2605,7 +2605,7 @@

Your drawers

pitch: { min: 13.5, max: 200 }, topCutoff: { min: 0.1, max: 1 }, connClr: { min: 0, max: 1, dovetail: 0.3, snapTop: 0.3, puzzleKey: 0.8, - smallPitch: 20, small: 0.3, puzzleRoom: 6.5 }, + smallPitch: 20, puzzlePitch: 20.7, small: 0.3, puzzleRoom: 6.5 }, bottomPad: { min: 0, max: 20 }, magnetD: { min: 1 }, magnetH: { min: 0.5, max: 10 }, screwHoleD: { min: 1 }, screwHeadD: { min: 1 }, screwHeadDepth: { min: 0.5, max: 10 }, @@ -2620,12 +2620,12 @@

Your drawers

} /* The fit clearance's ceiling for one design, as { max, by }; `by` says what sets it, for * the page to put in words: 'dovetail', 'snaptop', 'pitch', 'joint', or 'slip' for the - * plain 1 mm. One function, because four things have to agree on it — the field's range, - * the clamp on a number a link brings in, the message saying why, and the fit coupon's - * slackest pair — and it takes the whole design because the answer depends on the joint, - * which way its key goes in and the pitch. It took a connector name, and a 1 mm ceiling - * for everything but the dovetail let a snap clip's housing into the next piece and let - * small pitches leak. + * plain 1 mm, and with 'pitch', `below` is the pitch its band runs up to. One function, + * because four things have to agree on it — the field's range, the clamp on a number a + * link brings in, the message saying why, and the fit coupon's slackest pair — and it + * takes the whole design because the answer depends on the joint, which way its key goes + * in and the pitch. It took a connector name, and a 1 mm ceiling for everything but the + * dovetail let a snap clip's housing into the next piece and let small pitches leak. * * Measured first on 2 × 2 and 1-cell pieces from 13.5 to 60 mm, every 0.5 mm of pitch — * at every 0.05 of clearance for the puzzle, the bowtie and the puzzle key, at six for @@ -2654,13 +2654,18 @@

Your drawers

* pair did the same for any field from 0.3 up. 0.3 holds the wall one BLOAT, * 0.05, inside the piece, the overlap the cell regions keep; the coupon's * slackest pair is this ceiling cut the key's way, so it stops there too. - * pitch 0.3 below 20 mm for the puzzle and a puzzle key in the floor, earned. Past - * it they left holes from 13.5 to 19 mm — a puzzle key at 14 from 0.4, a - * puzzle at 19 from 0.35 — which cutAgain closes, and 0.35 builds clean at - * every pitch; but on 1-cell pieces a line across the pitch that climbs with - * the field puts the cut on the edge of the band two cells share, and the - * shells share it: a puzzle key from 0.37 (at 18.07 mm), a puzzle tab from - * 0.38 (at 18.54). The puzzle's lobe also needs room in its cell: it opens the + * pitch 0.3 for a puzzle below 20.7 mm and a puzzle key in the floor below 20, + * earned. Past it they left holes from 13.5 to 19 mm — a puzzle key at 14 + * from 0.4, a puzzle at 19 from 0.35 — which cutAgain closes, and 0.35 builds + * clean at every pitch; but on 1-cell pieces a line across the pitch that + * climbs with the field puts the cut on the edge of the band two cells share, + * and the shells share it: a puzzle key from 0.37 (at 18.07 mm), a puzzle tab + * from 0.38 (at 18.54). The puzzle's line climbs 0.05 every 0.16 mm and runs + * on past 20 — 0.8 at 19.89, 0.85 at 20.05, 0.9 at 20.21 — out of the field, + * 1 at 20.53 and 1.05 at 20.69, so its band runs to 20.7, where the plain + * 1 mm has its step to spare. Off the line the field builds clean from 20 to + * 20.7, and a ceiling that followed the line would gain only there; one band + * is simpler. The puzzle's lobe also needs room in its cell: it opens the * plate once its clearance passes half the pitch less 6.475 mm, so it stops at * half the pitch less 6.5 — 0.25 at 13.5, and 0.3 from 13.6. A bowtie in the * floor was held here too, for holes at 18 mm at 1. It builds clean at every @@ -2674,20 +2679,21 @@

Your drawers

* slip 1 for the rest. A key in the walls is cut to keySlim's own clearance, which * the field does not move. The snap and the H-clip did not leak above 0.3 at * any pitch measured but in one band — an H-clip dropped in from above leaks - * at 0.74 to 0.745 and nowhere else. From 20 mm up the bowtie and the puzzle - * leak only at isolated values: the bowtie at 25 mm at 0.64, the puzzle at - * 0.39–0.4 at 21 and 32.5–35 mm and here and there from 0.52 to 0.87. That is - * the sliver class again, not a ceiling — no limit short of 0.38 misses them, - * and the same class turns up under the default too: a snap at 20 mm at 0.29, - * a bowtie at 17 mm at 0.15 and under. One is not isolated: on 2 × 2 pieces - * a puzzle at 0.35 exactly, and not 0.34 or 0.36, has two shells sharing an - * edge at every pitch from 31.6 to 52.2 mm, where a corner of a notch's lobe, - * centred on the line between two cells, stands on the edge of the band they - * share. That wants mending in the cut; no ceiling misses a single value. + * at 0.74 to 0.745 and nowhere else. From 20 mm up (20.7 for the puzzle) the + * bowtie and the puzzle leak only at isolated values: the bowtie at 25 mm at + * 0.64, the puzzle at 0.39–0.4 at 21 and 32.5–35 mm and here and there from + * 0.52 to 0.87. That is the sliver class again, not a ceiling — no limit + * short of 0.38 misses them, and the same class turns up under the default + * too: a snap at 20 mm at 0.29, a bowtie at 17 mm at 0.15 and under. One is + * not isolated: on 2 × 2 pieces a puzzle at 0.35 exactly, and not 0.34 or + * 0.36, has two shells sharing an edge at every pitch from 31.6 to 52.2 mm, + * where a corner of a notch's lobe, centred on the line between two cells, + * stands on the edge of the band they share. That wants mending in the cut + * (issue #72); no ceiling misses a single value. * * test/plate-audit.js builds the step that earns each ceiling, which has to leak or cross - * the seam and be refused, and the bowtie's step to spare, which has to build closed and - * be refused. */ + * the seam and be refused, and the bowtie's step to spare and the step under the puzzle's + * line past 20 mm, which have to build closed and be refused. */ function connClrCeiling(cfg) { const R = PLATE_RANGES.connClr; const kind = jointKind(cfg.connector, cfg.keyMount, cfg.keyInsert); @@ -2695,10 +2701,11 @@

Your drawers

if (kind === 'snaptop') return { max: R.snapTop, by: 'snaptop' }; const floorKey = cfg.connector === 'puzzlekey' && cfg.keyMount !== 'wall'; if (cfg.connector !== 'puzzle' && !floorKey) return { max: R.max, by: 'slip' }; - if (cfg.pitch < R.smallPitch) { + const below = cfg.connector === 'puzzle' ? R.puzzlePitch : R.smallPitch; + if (cfg.pitch < below) { const room = cfg.connector === 'puzzle' ? Math.floor((cfg.pitch / 2 - R.puzzleRoom) * 100 + 1e-9) / 100 : Infinity; - return { max: Math.min(R.small, room), by: 'pitch' }; + return { max: Math.min(R.small, room), by: 'pitch', below }; } if (cfg.connector === 'puzzlekey') return { max: R.puzzleKey, by: 'joint' }; return { max: R.max, by: 'slip' }; diff --git a/index.html b/index.html index 04ab7c0..04d20a0 100644 --- a/index.html +++ b/index.html @@ -2392,7 +2392,7 @@

Your drawers

pitch: { min: 13.5, max: 200 }, topCutoff: { min: 0.1, max: 1 }, connClr: { min: 0, max: 1, dovetail: 0.3, snapTop: 0.3, puzzleKey: 0.8, - smallPitch: 20, small: 0.3, puzzleRoom: 6.5 }, + smallPitch: 20, puzzlePitch: 20.7, small: 0.3, puzzleRoom: 6.5 }, bottomPad: { min: 0, max: 20 }, magnetD: { min: 1 }, magnetH: { min: 0.5, max: 10 }, screwHoleD: { min: 1 }, screwHeadD: { min: 1 }, screwHeadDepth: { min: 0.5, max: 10 }, @@ -2407,12 +2407,12 @@

Your drawers

} /* The fit clearance's ceiling for one design, as { max, by }; `by` says what sets it, for * the page to put in words: 'dovetail', 'snaptop', 'pitch', 'joint', or 'slip' for the - * plain 1 mm. One function, because four things have to agree on it — the field's range, - * the clamp on a number a link brings in, the message saying why, and the fit coupon's - * slackest pair — and it takes the whole design because the answer depends on the joint, - * which way its key goes in and the pitch. It took a connector name, and a 1 mm ceiling - * for everything but the dovetail let a snap clip's housing into the next piece and let - * small pitches leak. + * plain 1 mm, and with 'pitch', `below` is the pitch its band runs up to. One function, + * because four things have to agree on it — the field's range, the clamp on a number a + * link brings in, the message saying why, and the fit coupon's slackest pair — and it + * takes the whole design because the answer depends on the joint, which way its key goes + * in and the pitch. It took a connector name, and a 1 mm ceiling for everything but the + * dovetail let a snap clip's housing into the next piece and let small pitches leak. * * Measured first on 2 × 2 and 1-cell pieces from 13.5 to 60 mm, every 0.5 mm of pitch — * at every 0.05 of clearance for the puzzle, the bowtie and the puzzle key, at six for @@ -2441,13 +2441,18 @@

Your drawers

* pair did the same for any field from 0.3 up. 0.3 holds the wall one BLOAT, * 0.05, inside the piece, the overlap the cell regions keep; the coupon's * slackest pair is this ceiling cut the key's way, so it stops there too. - * pitch 0.3 below 20 mm for the puzzle and a puzzle key in the floor, earned. Past - * it they left holes from 13.5 to 19 mm — a puzzle key at 14 from 0.4, a - * puzzle at 19 from 0.35 — which cutAgain closes, and 0.35 builds clean at - * every pitch; but on 1-cell pieces a line across the pitch that climbs with - * the field puts the cut on the edge of the band two cells share, and the - * shells share it: a puzzle key from 0.37 (at 18.07 mm), a puzzle tab from - * 0.38 (at 18.54). The puzzle's lobe also needs room in its cell: it opens the + * pitch 0.3 for a puzzle below 20.7 mm and a puzzle key in the floor below 20, + * earned. Past it they left holes from 13.5 to 19 mm — a puzzle key at 14 + * from 0.4, a puzzle at 19 from 0.35 — which cutAgain closes, and 0.35 builds + * clean at every pitch; but on 1-cell pieces a line across the pitch that + * climbs with the field puts the cut on the edge of the band two cells share, + * and the shells share it: a puzzle key from 0.37 (at 18.07 mm), a puzzle tab + * from 0.38 (at 18.54). The puzzle's line climbs 0.05 every 0.16 mm and runs + * on past 20 — 0.8 at 19.89, 0.85 at 20.05, 0.9 at 20.21 — out of the field, + * 1 at 20.53 and 1.05 at 20.69, so its band runs to 20.7, where the plain + * 1 mm has its step to spare. Off the line the field builds clean from 20 to + * 20.7, and a ceiling that followed the line would gain only there; one band + * is simpler. The puzzle's lobe also needs room in its cell: it opens the * plate once its clearance passes half the pitch less 6.475 mm, so it stops at * half the pitch less 6.5 — 0.25 at 13.5, and 0.3 from 13.6. A bowtie in the * floor was held here too, for holes at 18 mm at 1. It builds clean at every @@ -2461,20 +2466,21 @@

Your drawers

* slip 1 for the rest. A key in the walls is cut to keySlim's own clearance, which * the field does not move. The snap and the H-clip did not leak above 0.3 at * any pitch measured but in one band — an H-clip dropped in from above leaks - * at 0.74 to 0.745 and nowhere else. From 20 mm up the bowtie and the puzzle - * leak only at isolated values: the bowtie at 25 mm at 0.64, the puzzle at - * 0.39–0.4 at 21 and 32.5–35 mm and here and there from 0.52 to 0.87. That is - * the sliver class again, not a ceiling — no limit short of 0.38 misses them, - * and the same class turns up under the default too: a snap at 20 mm at 0.29, - * a bowtie at 17 mm at 0.15 and under. One is not isolated: on 2 × 2 pieces - * a puzzle at 0.35 exactly, and not 0.34 or 0.36, has two shells sharing an - * edge at every pitch from 31.6 to 52.2 mm, where a corner of a notch's lobe, - * centred on the line between two cells, stands on the edge of the band they - * share. That wants mending in the cut; no ceiling misses a single value. + * at 0.74 to 0.745 and nowhere else. From 20 mm up (20.7 for the puzzle) the + * bowtie and the puzzle leak only at isolated values: the bowtie at 25 mm at + * 0.64, the puzzle at 0.39–0.4 at 21 and 32.5–35 mm and here and there from + * 0.52 to 0.87. That is the sliver class again, not a ceiling — no limit + * short of 0.38 misses them, and the same class turns up under the default + * too: a snap at 20 mm at 0.29, a bowtie at 17 mm at 0.15 and under. One is + * not isolated: on 2 × 2 pieces a puzzle at 0.35 exactly, and not 0.34 or + * 0.36, has two shells sharing an edge at every pitch from 31.6 to 52.2 mm, + * where a corner of a notch's lobe, centred on the line between two cells, + * stands on the edge of the band they share. That wants mending in the cut + * (issue #72); no ceiling misses a single value. * * test/plate-audit.js builds the step that earns each ceiling, which has to leak or cross - * the seam and be refused, and the bowtie's step to spare, which has to build closed and - * be refused. */ + * the seam and be refused, and the bowtie's step to spare and the step under the puzzle's + * line past 20 mm, which have to build closed and be refused. */ function connClrCeiling(cfg) { const R = PLATE_RANGES.connClr; const kind = jointKind(cfg.connector, cfg.keyMount, cfg.keyInsert); @@ -2482,10 +2488,11 @@

Your drawers

if (kind === 'snaptop') return { max: R.snapTop, by: 'snaptop' }; const floorKey = cfg.connector === 'puzzlekey' && cfg.keyMount !== 'wall'; if (cfg.connector !== 'puzzle' && !floorKey) return { max: R.max, by: 'slip' }; - if (cfg.pitch < R.smallPitch) { + const below = cfg.connector === 'puzzle' ? R.puzzlePitch : R.smallPitch; + if (cfg.pitch < below) { const room = cfg.connector === 'puzzle' ? Math.floor((cfg.pitch / 2 - R.puzzleRoom) * 100 + 1e-9) / 100 : Infinity; - return { max: Math.min(R.small, room), by: 'pitch' }; + return { max: Math.min(R.small, room), by: 'pitch', below }; } if (cfg.connector === 'puzzlekey') return { max: R.puzzleKey, by: 'joint' }; return { max: R.max, by: 'slip' }; @@ -7504,23 +7511,24 @@

Your drawers

label: 'Screw head depth', when: () => state.screws, why: bossDepth }, // each joint's ceiling is its own, so none is held to another's reason; see clrWhy connClr: { min: RANGES.connClr.min, max: () => connClrCeiling(state).max, label: 'Fit clearance', - when: () => state.connector !== 'none', why: () => clrWhy(connClrCeiling(state).by) }, + when: () => state.connector !== 'none', why: () => clrWhy(connClrCeiling(state)) }, }; /* What sets the clearance's ceiling, in words: core.js connClrCeiling decides it and says which reason applies. A ceiling that moves with the pitch names the pitch, as the mount - sizes do, because that is the number to change; 'slip' is the plain 1 mm and keeps the - millimetres advice. The puzzle and the puzzle key said they opened holes in the plate, - as they did until a joint's cut was taken again when it came out open (cutAgain in - core.js); past each ceiling now two cells' shells share an edge at some pitches, which - is not watertight either but is no hole. The dovetail said its pocket broke through - into the socket beside it, which it does from 0.25, under its ceiling. */ + sizes do, because that is the number to change, and the pitch its band runs up to, + which for the puzzle is its own; 'slip' is the plain 1 mm and keeps the millimetres + advice. The puzzle and the puzzle key said they opened holes in the plate, as they did + until a joint's cut was taken again when it came out open (cutAgain in core.js); past + each ceiling now two cells' shells share an edge at some pitches, which is not + watertight either but is no hole. The dovetail said its pocket broke through into the + socket beside it, which it does from 0.25, under its ceiling. */ const CLR_JOINT = { puzzle: 'puzzle tab', puzzlekey: 'puzzle key' }; -const clrWhy = (by) => ({ +const clrWhy = ({ by, below }) => ({ dovetail: '— any looser and a dovetail pocket leaves the plate not watertight at some ' + 'pitches', snaptop: '— any looser and the housing of a snap clip dropped in from above runs up to ' + 'the seam and on into the next piece', - pitch: `${atPitch()} — on cells under ${RANGES.connClr.smallPitch} mm a ` + + pitch: `${atPitch()} — on cells under ${below} mm a ` + `looser ${CLR_JOINT[state.connector]} leaves the plate not watertight`, joint: `— any looser and a ${CLR_JOINT[state.connector]}'s recess leaves the plate not ` + 'watertight at some pitches', diff --git a/sw.js b/sw.js index c43f938..eb73b81 100644 --- a/sw.js +++ b/sw.js @@ -52,7 +52,7 @@ load from this site, and the app's icons and manifest, as paths from the root. VERSION is a hash of all of those files, names and bytes, and of this script as it is served, FILES filled in and everything else but VERSION itself. */ -const VERSION = "9ddeb0107071"; +const VERSION = "4f0cfa58126c"; const FILES = [ "./", "bins/", From 5f0bd5fdfe31d934606cc2d9b0f157ac060644c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 13:31:32 +0000 Subject: [PATCH 08/58] Cut no counterbore under a screw head too narrow to clear its shank's corners, so the plate stays watertight Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 7 +++++- src/core.js | 26 +++++++++++++++++----- src/ui.js | 10 ++++++++- test/plate-audit.js | 36 +++++++++++++++++++++++++++++++ test/ui/plates-robustness.spec.js | 27 +++++++++++++++++++++++ 5 files changed, 99 insertions(+), 7 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index a0c478a..fb546d5 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -702,7 +702,12 @@ currently installed): which the field already sizes with room round it. `MOUNT_BORE` says each bore's sides and corner radius, and the largest size whose corners fit a room; `fastenerCutter` cuts what it says, and `mountLimits`, which measures every room to the corners, reads its - sizes back from it, so the shape is decided in one place. + sizes back from it, so the shape is decided in one place. A counterbore is cut only over + a shank it clears, its flats outside the shank's corners: the hole over cos(π/12), and + 0.01 mm more. Narrower, the 14 flats cross the 12 corners and the two cuts' walls weave + round the hole, and here and there a cell leaked by the hundred (#73: 2.03 mm over 2 + with the corners on the sizes, 2.046 over 2 and 1.034 over 1 with the flats). Such a + head is cut as none, as one no wider than the hole always was. - **The socket's corner clearance is not uniform.** Known, documented, deliberately not fixed — see [socket-clearance.md](socket-clearance.md). - **The plate's outer corner radius has a ceiling, and it is the socket's.** Both the diff --git a/src/core.js b/src/core.js index 2602c9f..cb7506a 100644 --- a/src/core.js +++ b/src/core.js @@ -833,12 +833,28 @@ function cylinder(cx, cy, r, z0, z1, seg) { * default 6 mm head and 2.90 for the 3 mm hole. Now their corners go out the same * way, and no further: the magnet's 0.1 is a press fit, and a screw head is not * pressed in. It drops into its counterbore, which the field already sizes with room - * round it (6 mm for an M3's 5.5 mm head), and the shank is a clearance hole. */ + * round it (6 mm for an M3's 5.5 mm head), and the shank is a clearance hole. + * + * A counterbore is cut only over a shank it clears (head.cuts): its flats stand outside + * the shank's corners, the head the hole over cos(π/12), and 0.01 mm more (head.over). + * Narrower, the 14 flats cross the 12 corners, the two cuts' walls weave round the hole + * instead of one standing inside the other, and here and there a cell leaks by the + * hundred: 1.034 mm over 1, 2.063 over 2 (#73, where with the corners on the sizes it was + * 2.03 over 2). Measured on one-cell plates at eight pitches from 34.76 to 60 mm, shanks + * from 1 to 20 mm, every 0.001 mm from a hundredth under the corner circle to two past + * it and every 0.01 from the shank up to a tenth past: the last to leak by the circle + * was 0.003 mm past it (20.709 over 20), and from 0.004 nothing did but one head at one + * pitch, 3.59 over 3.4 at 60, open in the underside, the sliver class that comes and + * goes with a size. A head under the line is cut as none, as one no wider than the hole + * always was: within 3.5% of the hole it is no seat for a screw head, and all it would + * add is slivers between the shank's corners. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, head: { sides: 14, r: (d) => d/2 / Math.cos(Math.PI/14), - fits: (room) => 2 * room * Math.cos(Math.PI / 14) }, + fits: (room) => 2 * room * Math.cos(Math.PI / 14), + over: (hole) => 2 * MOUNT_BORE.hole.r(hole) + 0.01, + cuts: (d, hole) => d >= MOUNT_BORE.head.over(hole) }, hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), fits: (room) => 2 * room * Math.cos(Math.PI / 12) }, }; @@ -872,7 +888,7 @@ function fastenerCutter(cfg, magZ0, magZ1, shankTop) { ? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { - if (cfg.screwHeadD > cfg.screwHoleD) + if (head.cuts(cfg.screwHeadD, cfg.screwHoleD)) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); } @@ -895,7 +911,7 @@ function movePolys(polys, dx, dy) { function screwCutter(cx, cy, holeD, headD, z0, z1, headDepth, fromTop) { const { head, hole } = MOUNT_BORE; let polys = cylinder(cx, cy, hole.r(holeD), z0 - 0.5, z1 + 0.5, hole.sides); - if (headD > holeD) { + if (head.cuts(headD, holeD)) { const rec = fromTop ? cylinder(cx, cy, head.r(headD), z1 - headDepth, z1 + 0.5, head.sides) : cylinder(cx, cy, head.r(headD), z0 - 0.5, z0 + headDepth, head.sides); @@ -2361,7 +2377,7 @@ function platePad(cfg) { if ((cfg.magnets || cfg.screws) && cfg.baseMode !== 'bosses') { pad = Math.max(pad, cfg.magnetBase || 2.8); if (cfg.magnets) pad = Math.max(pad, under(cfg.magnetH)); - if (cfg.screws && cfg.screwHeadD > cfg.screwHoleD) + if (cfg.screws && MOUNT_BORE.head.cuts(cfg.screwHeadD, cfg.screwHoleD)) pad = Math.max(pad, under(cfg.screwHeadDepth)); } const keyedConn = ['bowtie', 'snap', 'puzzlekey'].includes(cfg.connector); diff --git a/src/ui.js b/src/ui.js index b951c94..71582f8 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1764,7 +1764,15 @@ function readmeText() { lines.push(`Split: ${splitName()} | Pieces: ${layout.pieces.length} in ${plural(rows, 'row band')}`); lines.push(`Connectors: ${state.connector}` + (state.connector === 'dovetail' ? ` (clearance ${state.tab.clr} mm/side)` : '')); if (state.magnets) lines.push(`Magnets: ${state.magnetD} x ${state.magnetH} mm, from ${state.magnetSide}`); - if (state.screws) lines.push(`Screws: ${state.screwHoleD} mm holes, ${state.screwHeadD} mm counterbore`); + /* A head that does not clear the hole's corners is cut as none (MOUNT_BORE in core.js), + and one no wider than the hole always was; the README says so, and for a head that + was meant as a counterbore, the size that would be one. */ + const { head } = MOUNT_BORE; + if (state.screws) lines.push(`Screws: ${state.screwHoleD} mm holes, ` + + (head.cuts(state.screwHeadD, state.screwHoleD) ? `${state.screwHeadD} mm counterbore` + : 'no counterbore' + (state.screwHeadD <= state.screwHoleD ? '' + : ` (a head clears a ${state.screwHoleD} mm hole from ` + + `${Math.ceil(head.over(state.screwHoleD) * 100 - 1e-9) / 100} mm)`))); /* The figure the dialog quotes, said the way it says it, with the price it was worked out at: the README is read away from the page. The ZIP is only made once every piece exists, so there is always a total to give. */ diff --git a/test/plate-audit.js b/test/plate-audit.js index 78d9fb8..7e7fc07 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -2003,6 +2003,42 @@ console.log('\nthe other limits, built at their ends:'); console.log(` ${nm.padEnd(28)} ${leakText(r)}`); if (r.bad) bad++; } + /* A screw head only a hair wider than its shank (#73). The counterbore's 14 flats + crossed the shank's 12 corners, and here and there a cell leaked by the hundred: on + main, 2.03 mm over 2 left 196 edges a piece at 37.67 mm; with the flats on the sizes, + 2.046 over 2 left 310 on these four cells and 1.034 over 1 at 42 mm 828. A head is cut + as a counterbore only once its flats clear the shank's corners, the hole over + cos(π/12) and 0.01 mm more (MOUNT_BORE in core.js), worked out here rather than read + from it. Under that it is cut as none: the plate has to be the very one a head no + wider than the hole builds, to the byte, a thousandth under the line too. The + narrowest head cut, a thousandth past it, has to build watertight and not be that + plate. */ + { + const crypto = require('crypto'); + const digest = (r) => { + const h = crypto.createHash('sha256'); + for (const polys of r.pieces) h.update(Buffer.from(G.stlBinary(polys, 'p'))); + return h.digest('hex'); + }; + const line = (d) => d / Math.cos(Math.PI / 12) + 0.01; + const none = new Map(); + for (const [nm, p, d, h, cut] of [ + ['2.03 mm head over 2, 37.67', 37.67, 2, 2.03, false], + ['2.046 mm head over 2, 37.67', 37.67, 2, 2.046, false], + ['1.034 mm head over 1, 42', 42, 1, 1.034, false], + ['just under the line, over 2', 37.67, 2, Math.floor(line(2) * 1000) / 1000, false], + ['narrowest head over 2', 37.67, 2, Math.ceil(line(2) * 1000) / 1000, true], + ]) { + const at = { pitch: p, drawerW: 2 * p, drawerD: 2 * p, connector: 'none', screws: true, screwHoleD: d }; + if (!none.has(`${p} ${d}`)) none.set(`${p} ${d}`, digest(buildAll({ ...at, screwHeadD: d }))); + const r = buildAll({ ...at, screwHeadD: h }); + const same = digest(r) === none.get(`${p} ${d}`); + console.log(` ${nm.padEnd(28)} ${h} mm: ${leakText(r)}, ` + (same + ? (cut ? 'NO COUNTERBORE CUT' : 'cut as no counterbore') + : (cut ? 'a counterbore' : 'A COUNTERBORE CUT'))); + if (r.bad || same === cut) bad++; + } + } /* A pocket against a joint's cut in the floor (#64). A bowtie housed in the floor at 42 mm, with magnets from beneath, built 12 bad edges at 7.9 mm and 19 at 10 with Download on: the pocket reached the key's recess, and with both ceilings 2 mm up the diff --git a/test/ui/plates-robustness.spec.js b/test/ui/plates-robustness.spec.js index e86b832..e32d6b5 100644 --- a/test/ui/plates-robustness.spec.js +++ b/test/ui/plates-robustness.spec.js @@ -473,6 +473,33 @@ test.describe('the clearance ceiling is the joint\'s and the pitch\'s', () => { }); }); +/* ---- #73: a screw head only a hair wider than its shank ---------------------------- */ +/* Its counterbore's 14 flats crossed the shank's 12 corners, and the plate leaked by the + hundred with nothing said and Download on: on main, a 2.03 mm head over a 2 mm shank + left 196 edges in each of two pieces at 37.67 mm. A head that does not clear the shank's + corners is cut as none now, as one no wider than the hole always was (MOUNT_BORE in + core.js): within 3.5% of the hole it is no seat for a screw head. So the link builds + with Download on and nothing in Checks, and the README says there is no counterbore + and from what size there would be one. 2.1 mm clears a 2 mm hole and is cut. */ +test.describe('a screw head that does not clear its shank is cut as none', () => { + const AT = '#pi=37.67&w=150.68&d=75.34&mm=custom&ml=0&mr=0&mf=0&mb=0&bw=91.34&bd=400' + + '&sc=1&sh=2&cn=none'; + for (const [sd, says] of [ + ['2.03', 'Screws: 2 mm holes, no counterbore (a head clears a 2 mm hole from 2.09 mm)'], + ['2.1', 'Screws: 2 mm holes, 2.1 mm counterbore'], + ]) + test(`a ${sd} mm head over a 2 mm shank builds, and the README says what was cut`, + async ({ page }) => { + const errors = await openAt(page, `${AT}&sd=${sd}`); + expect(await text(page, 'pieceTail')).toMatch(/ready/); + expect(await shown(page, 'errScrew')).toBe(false); + expect(await exportOff(page)).toBe(false); + const readme = (await page.evaluate(() => readmeText())).split('\n'); + expect(readme).toContain(says); + expect(errors).toEqual([]); + }); +}); + test('a failed build says so in the table and the dialog, and Download goes off', async ({ page }) => { const pageErrors = []; From b3787f6fe225b191db6b443191eae2f4f2e9f95a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 13:36:52 +0000 Subject: [PATCH 09/58] Keep the test tile watertight with magnets and screws at their usual sizes Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- src/core.js | 9 ++++++++- test/plate-audit.js | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/core.js b/src/core.js index cb7506a..dcc4e8d 100644 --- a/src/core.js +++ b/src/core.js @@ -888,7 +888,14 @@ function fastenerCutter(cfg, magZ0, magZ1, shankTop) { ? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { - if (head.cuts(cfg.screwHeadD, cfg.screwHoleD)) + /* A counterbore the magnet pocket from beneath holds whole is that pocket already: both + 14 sides, corners on the same rays, no deeper. Since the bores stand their flats on + the sizes, the default 6 mm head's walls stand 0.022 mm inside the 6 mm magnet's, + and unioned, the test tile at 42 mm with magnets and screws left 6 edges open in + its underside. Left out, the pocket is the same solid, and closed. */ + const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && + head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; + if (head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); } diff --git a/test/plate-audit.js b/test/plate-audit.js index 7e7fc07..310e411 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -1037,6 +1037,14 @@ console.log('\nloose parts and samples, watertight and oriented:'); drawerW: G.DEFAULTS.pitch, drawerD: G.DEFAULTS.pitch, marginMode: 'custom', mLeft: 0, mRight: 0, mFront: 0, mBack: 0 }); report('test tile', G.buildTestTile(tileCfg).polys); + /* The tile with magnets and screws, the page's own sizes. Once a screw's bores stood + their flats on the sizes, the 6 mm counterbore's walls stood 0.022 mm inside the 6 mm + magnet pocket's from beneath, and the tile left 6 edges open in its underside; a + counterbore the magnet pocket holds whole is not cut now (fastenerCutter). A magnet + from above is cut from the other face, and that tile is built too. */ + for (const side of ['bottom', 'top']) + report(`tile, magnets ${side === 'top' ? 'above' : 'below'}, screws`, + G.buildTestTile({ ...tileCfg, magnets: true, magnetSide: side, screws: true }).polys); for (const conn of CONNECTORS) for (const keyInsert of ['bottom', 'top']) { @@ -2518,9 +2526,11 @@ console.log('\nplates without half cells build the same bytes:'); // fanCentre moved four of its nine cells' bottom caps off a spoke a few thousandths from // a cutter's corner: the same bottom face fanned from another point, its area and the // volume unchanged. Then the screw's bores went out until their flats stood on the - // sizes asked, the counterbore and the shank wider by design + // sizes asked, the counterbore and the shank wider by design; then the counterbore the + // magnet pocket from beneath holds whole was left out of the cut, the same pocket with + // its faces no longer split by the head's walls (54138.661 mm³ to 54138.679) ['126 x 126, magnets and screws', { drawerW: 126, drawerD: 126, marginMode: 'custom', - mLeft: 0, mRight: 0, mFront: 0, mBack: 0, magnets: true, screws: true }, '7eaec017027d5a46'], + mLeft: 0, mRight: 0, mFront: 0, mBack: 0, magnets: true, screws: true }, 'e964beefa8b378bf'], ['140 x 140, corner pockets', { drawerW: 140, drawerD: 140, marginMode: 'auto', magnets: true, baseMode: 'bosses' }, '7088f24def428095'], ['168 x 180, skeleton', { drawerW: 168, drawerD: 180, marginMode: 'auto', From 3a54fd5e366d989e543e0ed10472e7c060b9e1af Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 13:36:53 +0000 Subject: [PATCH 10/58] Rebuild the pages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 33 ++++++++++++++++++++++++++++----- index.html | 43 +++++++++++++++++++++++++++++++++++++------ sw.js | 2 +- 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/bins/index.html b/bins/index.html index 6119e71..331f553 100644 --- a/bins/index.html +++ b/bins/index.html @@ -2439,12 +2439,28 @@

Your drawers

* default 6 mm head and 2.90 for the 3 mm hole. Now their corners go out the same * way, and no further: the magnet's 0.1 is a press fit, and a screw head is not * pressed in. It drops into its counterbore, which the field already sizes with room - * round it (6 mm for an M3's 5.5 mm head), and the shank is a clearance hole. */ + * round it (6 mm for an M3's 5.5 mm head), and the shank is a clearance hole. + * + * A counterbore is cut only over a shank it clears (head.cuts): its flats stand outside + * the shank's corners, the head the hole over cos(π/12), and 0.01 mm more (head.over). + * Narrower, the 14 flats cross the 12 corners, the two cuts' walls weave round the hole + * instead of one standing inside the other, and here and there a cell leaks by the + * hundred: 1.034 mm over 1, 2.063 over 2 (#73, where with the corners on the sizes it was + * 2.03 over 2). Measured on one-cell plates at eight pitches from 34.76 to 60 mm, shanks + * from 1 to 20 mm, every 0.001 mm from a hundredth under the corner circle to two past + * it and every 0.01 from the shank up to a tenth past: the last to leak by the circle + * was 0.003 mm past it (20.709 over 20), and from 0.004 nothing did but one head at one + * pitch, 3.59 over 3.4 at 60, open in the underside, the sliver class that comes and + * goes with a size. A head under the line is cut as none, as one no wider than the hole + * always was: within 3.5% of the hole it is no seat for a screw head, and all it would + * add is slivers between the shank's corners. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, head: { sides: 14, r: (d) => d/2 / Math.cos(Math.PI/14), - fits: (room) => 2 * room * Math.cos(Math.PI / 14) }, + fits: (room) => 2 * room * Math.cos(Math.PI / 14), + over: (hole) => 2 * MOUNT_BORE.hole.r(hole) + 0.01, + cuts: (d, hole) => d >= MOUNT_BORE.head.over(hole) }, hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), fits: (room) => 2 * room * Math.cos(Math.PI / 12) }, }; @@ -2478,7 +2494,14 @@

Your drawers

? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { - if (cfg.screwHeadD > cfg.screwHoleD) + /* A counterbore the magnet pocket from beneath holds whole is that pocket already: both + 14 sides, corners on the same rays, no deeper. Since the bores stand their flats on + the sizes, the default 6 mm head's walls stand 0.022 mm inside the 6 mm magnet's, + and unioned, the test tile at 42 mm with magnets and screws left 6 edges open in + its underside. Left out, the pocket is the same solid, and closed. */ + const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && + head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; + if (head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); } @@ -2501,7 +2524,7 @@

Your drawers

function screwCutter(cx, cy, holeD, headD, z0, z1, headDepth, fromTop) { const { head, hole } = MOUNT_BORE; let polys = cylinder(cx, cy, hole.r(holeD), z0 - 0.5, z1 + 0.5, hole.sides); - if (headD > holeD) { + if (head.cuts(headD, holeD)) { const rec = fromTop ? cylinder(cx, cy, head.r(headD), z1 - headDepth, z1 + 0.5, head.sides) : cylinder(cx, cy, head.r(headD), z0 - 0.5, z0 + headDepth, head.sides); @@ -3967,7 +3990,7 @@

Your drawers

if ((cfg.magnets || cfg.screws) && cfg.baseMode !== 'bosses') { pad = Math.max(pad, cfg.magnetBase || 2.8); if (cfg.magnets) pad = Math.max(pad, under(cfg.magnetH)); - if (cfg.screws && cfg.screwHeadD > cfg.screwHoleD) + if (cfg.screws && MOUNT_BORE.head.cuts(cfg.screwHeadD, cfg.screwHoleD)) pad = Math.max(pad, under(cfg.screwHeadDepth)); } const keyedConn = ['bowtie', 'snap', 'puzzlekey'].includes(cfg.connector); diff --git a/index.html b/index.html index 04d20a0..0b3126d 100644 --- a/index.html +++ b/index.html @@ -2226,12 +2226,28 @@

Your drawers

* default 6 mm head and 2.90 for the 3 mm hole. Now their corners go out the same * way, and no further: the magnet's 0.1 is a press fit, and a screw head is not * pressed in. It drops into its counterbore, which the field already sizes with room - * round it (6 mm for an M3's 5.5 mm head), and the shank is a clearance hole. */ + * round it (6 mm for an M3's 5.5 mm head), and the shank is a clearance hole. + * + * A counterbore is cut only over a shank it clears (head.cuts): its flats stand outside + * the shank's corners, the head the hole over cos(π/12), and 0.01 mm more (head.over). + * Narrower, the 14 flats cross the 12 corners, the two cuts' walls weave round the hole + * instead of one standing inside the other, and here and there a cell leaks by the + * hundred: 1.034 mm over 1, 2.063 over 2 (#73, where with the corners on the sizes it was + * 2.03 over 2). Measured on one-cell plates at eight pitches from 34.76 to 60 mm, shanks + * from 1 to 20 mm, every 0.001 mm from a hundredth under the corner circle to two past + * it and every 0.01 from the shank up to a tenth past: the last to leak by the circle + * was 0.003 mm past it (20.709 over 20), and from 0.004 nothing did but one head at one + * pitch, 3.59 over 3.4 at 60, open in the underside, the sliver class that comes and + * goes with a size. A head under the line is cut as none, as one no wider than the hole + * always was: within 3.5% of the hole it is no seat for a screw head, and all it would + * add is slivers between the shank's corners. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, head: { sides: 14, r: (d) => d/2 / Math.cos(Math.PI/14), - fits: (room) => 2 * room * Math.cos(Math.PI / 14) }, + fits: (room) => 2 * room * Math.cos(Math.PI / 14), + over: (hole) => 2 * MOUNT_BORE.hole.r(hole) + 0.01, + cuts: (d, hole) => d >= MOUNT_BORE.head.over(hole) }, hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), fits: (room) => 2 * room * Math.cos(Math.PI / 12) }, }; @@ -2265,7 +2281,14 @@

Your drawers

? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { - if (cfg.screwHeadD > cfg.screwHoleD) + /* A counterbore the magnet pocket from beneath holds whole is that pocket already: both + 14 sides, corners on the same rays, no deeper. Since the bores stand their flats on + the sizes, the default 6 mm head's walls stand 0.022 mm inside the 6 mm magnet's, + and unioned, the test tile at 42 mm with magnets and screws left 6 edges open in + its underside. Left out, the pocket is the same solid, and closed. */ + const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && + head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; + if (head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); } @@ -2288,7 +2311,7 @@

Your drawers

function screwCutter(cx, cy, holeD, headD, z0, z1, headDepth, fromTop) { const { head, hole } = MOUNT_BORE; let polys = cylinder(cx, cy, hole.r(holeD), z0 - 0.5, z1 + 0.5, hole.sides); - if (headD > holeD) { + if (head.cuts(headD, holeD)) { const rec = fromTop ? cylinder(cx, cy, head.r(headD), z1 - headDepth, z1 + 0.5, head.sides) : cylinder(cx, cy, head.r(headD), z0 - 0.5, z0 + headDepth, head.sides); @@ -3754,7 +3777,7 @@

Your drawers

if ((cfg.magnets || cfg.screws) && cfg.baseMode !== 'bosses') { pad = Math.max(pad, cfg.magnetBase || 2.8); if (cfg.magnets) pad = Math.max(pad, under(cfg.magnetH)); - if (cfg.screws && cfg.screwHeadD > cfg.screwHoleD) + if (cfg.screws && MOUNT_BORE.head.cuts(cfg.screwHeadD, cfg.screwHoleD)) pad = Math.max(pad, under(cfg.screwHeadDepth)); } const keyedConn = ['bowtie', 'snap', 'puzzlekey'].includes(cfg.connector); @@ -9140,7 +9163,15 @@

Your drawers

lines.push(`Split: ${splitName()} | Pieces: ${layout.pieces.length} in ${plural(rows, 'row band')}`); lines.push(`Connectors: ${state.connector}` + (state.connector === 'dovetail' ? ` (clearance ${state.tab.clr} mm/side)` : '')); if (state.magnets) lines.push(`Magnets: ${state.magnetD} x ${state.magnetH} mm, from ${state.magnetSide}`); - if (state.screws) lines.push(`Screws: ${state.screwHoleD} mm holes, ${state.screwHeadD} mm counterbore`); + /* A head that does not clear the hole's corners is cut as none (MOUNT_BORE in core.js), + and one no wider than the hole always was; the README says so, and for a head that + was meant as a counterbore, the size that would be one. */ + const { head } = MOUNT_BORE; + if (state.screws) lines.push(`Screws: ${state.screwHoleD} mm holes, ` + + (head.cuts(state.screwHeadD, state.screwHoleD) ? `${state.screwHeadD} mm counterbore` + : 'no counterbore' + (state.screwHeadD <= state.screwHoleD ? '' + : ` (a head clears a ${state.screwHoleD} mm hole from ` + + `${Math.ceil(head.over(state.screwHoleD) * 100 - 1e-9) / 100} mm)`))); /* The figure the dialog quotes, said the way it says it, with the price it was worked out at: the README is read away from the page. The ZIP is only made once every piece exists, so there is always a total to give. */ diff --git a/sw.js b/sw.js index eb73b81..33a028c 100644 --- a/sw.js +++ b/sw.js @@ -52,7 +52,7 @@ load from this site, and the app's icons and manifest, as paths from the root. VERSION is a hash of all of those files, names and bytes, and of this script as it is served, FILES filled in and everything else but VERSION itself. */ -const VERSION = "4f0cfa58126c"; +const VERSION = "2d0ca3035d3c"; const FILES = [ "./", "bins/", From 6bc8d5b36c44455fd3e53dc760067c719e4ef549 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 15:01:22 +0000 Subject: [PATCH 11/58] Keep plates watertight when a counterbore's corner lines up with a flat of its screw's shank Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 9 +++++++- src/core.js | 53 ++++++++++++++++++++++++++++++++++++++++----- test/plate-audit.js | 25 +++++++++++++++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index fb546d5..d19aa88 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -707,7 +707,14 @@ currently installed): 0.01 mm more. Narrower, the 14 flats cross the 12 corners and the two cuts' walls weave round the hole, and here and there a cell leaked by the hundred (#73: 2.03 mm over 2 with the corners on the sizes, 2.046 over 2 and 1.034 over 1 with the flats). Such a - head is cut as none, as one no wider than the hole always was. + head is cut as none, as one no wider than the hole always was. And the shank is turned + π/84 about its axis where one of the counterbore's corners would otherwise stand within + 0.005 mm of a flat's plane carried on past the shank (`MOUNT_BORE.hole.turn`): the BSP + splits a magnet pocket's floor or roof along that whole plane, the weld pulled the + corner onto the split, and the plate came out open by the dozen round every site (#74: + 136 edges a piece for 1.0353 mm under 1.0873 with a magnet from above at 42 mm, on + main). Turned, no corner comes within 0.0039 mm of a flat at any size, and a shank no + corner came near is cut as before. - **The socket's corner clearance is not uniform.** Known, documented, deliberately not fixed — see [socket-clearance.md](socket-clearance.md). - **The plate's outer corner radius has a ceiling, and it is the socket's.** Both the diff --git a/src/core.js b/src/core.js index dcc4e8d..081c91c 100644 --- a/src/core.js +++ b/src/core.js @@ -817,7 +817,8 @@ function cylinder(cx, cy, r, z0, z1, seg) { * stand for a size typed (`r`), and back the other way, the largest size whose corners * stay inside a room (`fits`). fastenerCutter and screwCutter cut these and mountLimits * holds the sizes to them, so a bore's shape is decided here and nowhere else. Each is - * cylinder's polygon about the site, a corner at every 2πk/sides from +x. + * cylinder's polygon about the site, a corner at every 2πk/sides from +x, but for a shank + * that `turn` moves (below). * * Every bore stands its flats on the size asked, so its corners are 1/cos(π/sides) of the * radius out and it is no narrower anywhere than the size typed. @@ -847,7 +848,31 @@ function cylinder(cx, cy, r, z0, z1, seg) { * pitch, 3.59 over 3.4 at 60, open in the underside, the sliver class that comes and * goes with a size. A head under the line is cut as none, as one no wider than the hole * always was: within 3.5% of the hole it is no seat for a screw head, and all it would - * add is slivers between the shank's corners. */ + * add is slivers between the shank's corners. + * + * Under a counterbore the shank is turned a little about its axis (`turn`, #74) where one + * of the counterbore's corners would stand on one of its flats carried on past the shank. + * The BSP splits what it meets along a flat's whole plane, not just the flat: the floor + * of a magnet pocket from above, which is a ring round the counterbore, or the roof of one + * from beneath round a counterbore deeper than it. A split a micron or two from the + * corner was welded onto it (healCsgSeams), which bent the counterbore's wall off its + * plane, and the plate came out open by the dozen round every site: 136 edges a piece at + * 42 mm for main's 1.0353 mm shank under a 1.0873 head with a 6 x 2 magnet from above, 36 + * a cell for 1.06 over 1 here, 62 for 3.305 over 3, 55 for 5.4959 over 3 under an 8 x 3 + * magnet. Such sizes come every few hundredths at any ratio of head to shank. A corner + * meets a flat's plane at an odd multiple of 15/7° off the flat's normal; turned π/84, + * 15/7°, it meets them at the even ones, so a size that put a corner on a flat puts it + * midway between two. The turn is taken where a corner comes nearer than 0.005 mm + * unturned and the turn leaves it further off, so no corner comes within 0.0039 mm of a + * flat at any size, and a shank no corner came near is cut as it was. Half a facet, π/12, + * clears the corners as well, but it squares two flats to the cell's edges, and at + * 37.67 mm those folded the socket floor beside the pocket. Measured on 3,846 one-cell + * plates with a magnet from above, 37.67 to 55 mm, shanks 1 to 5 mm and heads up to 1.15 + * times them, every 0.001 mm round these sizes: 546 open before, 4 now. On 1,800 random + * designs the turn moves, every kind of mount: 90 open before, 26 now. It closed 83 and + * opened 19, 14 of them at 42 mm with a magnet from beneath under a deeper counterbore, + * where one size in nine leaks unturned too, by the same slivers at the magnet pocket's + * corners in the underside. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, @@ -856,7 +881,19 @@ const MOUNT_BORE = { over: (hole) => 2 * MOUNT_BORE.hole.r(hole) + 0.01, cuts: (d, hole) => d >= MOUNT_BORE.head.over(hole) }, hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), - fits: (room) => 2 * room * Math.cos(Math.PI / 12) }, + fits: (room) => 2 * room * Math.cos(Math.PI / 12), + // how far to turn the shank under a counterbore headD across (see above) + turn: (d, headD) => { + const { head, hole } = MOUNT_BORE, R = head.r(headD); + const clear = (t) => { // the nearest a corner of the counterbore comes to a flat's plane + let g = Infinity; + for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) + g = Math.min(g, Math.abs(R * Math.cos(2*Math.PI*k/head.sides - (2*m + 1)*Math.PI/hole.sides - t) - d/2)); + return g; + }; + const now = clear(0); + return now < 0.005 && clear(Math.PI / 84) > now ? Math.PI / 84 : 0; + } }, }; /* Everything removed at one mounting site, as ONE solid. @@ -895,9 +932,15 @@ function fastenerCutter(cfg, magZ0, magZ1, shankTop) { its underside. Left out, the pocket is the same solid, and closed. */ const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; - if (head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet) + const counterbore = head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet; + if (counterbore) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); - add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); + // the shank, turned off the counterbore's corners where one would stand on a flat (#74) + const shank = cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides); + const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD) : 0; + const c = Math.cos(t), s = Math.sin(t), turn = (v) => [v[0]*c - v[1]*s, v[0]*s + v[1]*c, v[2]]; + add(t ? shank.map((p) => ({ verts: p.verts.map(turn), plane: { n: turn(p.plane.n), w: p.plane.w } })) + : shank); } return cut; } diff --git a/test/plate-audit.js b/test/plate-audit.js index 310e411..bec4933 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -2047,6 +2047,31 @@ console.log('\nthe other limits, built at their ends:'); if (r.bad || same === cut) bad++; } } + /* A counterbore with a corner on one of its shank's flats, carried on past the shank + (#74). The BSP split the magnet pocket's floor along that flat's plane a micron or + two from the corner, the weld moved the corner onto the split, and its wall left its + plane: main's 1.0353 mm shank under a 1.0873 head, with a 6 x 2 magnet from above, + left 136 edges in each of two pieces at 42 mm, and the same bores here, with their + flats on the sizes, are a 1 mm shank under 1.06 (36 a cell). 4.25 over 4 left 26 and + 3.305 over 3 62; 5.4959 over 3 under an 8 x 3 magnet, a head nearly twice the shank, + 55; and from beneath, the magnet's roof round a counterbore deeper than it, 1.06 over + 1 left 12. The shank is turned off the corners now (MOUNT_BORE.hole.turn). Each has + to build closed, with no folds. */ + for (const [nm, o] of [ + ['main\'s 1.0353 under 1.0873, 42', { pitch: 42, drawerW: 168, drawerD: 84, bedW: 100, bedD: 400, + screwHoleD: 1.0353 * Math.cos(Math.PI / 12), screwHeadD: 1.0873 * Math.cos(Math.PI / 14) }], + ['1.06 mm head over 1, 42', { screwHoleD: 1, screwHeadD: 1.06 }], + ['4.25 mm head over 4, 42', { screwHoleD: 4, screwHeadD: 4.25 }], + ['3.305 mm head over 3, 42', { screwHoleD: 3, screwHeadD: 3.305 }], + ['5.4959 over 3, 8 x 3 magnet', { screwHoleD: 3, screwHeadD: 5.4959, magnetD: 8, magnetH: 3 }], + ['1.06 over 1, from beneath', { screwHoleD: 1, screwHeadD: 1.06, magnetSide: 'bottom', screwHeadDepth: 3 }], + ]) { + const r = buildAll({ pitch: 42, drawerW: 42, drawerD: 42, connector: 'none', magnets: true, + magnetSide: 'top', screws: true, ...o }); + const folds = r.pieces.reduce((s, pp) => s + checkOrientation(pp).folds, 0); + console.log(` ${nm.padEnd(30)} ${leakText(r)}${folds ? `, ${folds} FOLDS` : ''}`); + if (r.bad || folds) bad++; + } /* A pocket against a joint's cut in the floor (#64). A bowtie housed in the floor at 42 mm, with magnets from beneath, built 12 bad edges at 7.9 mm and 19 at 10 with Download on: the pocket reached the key's recess, and with both ceilings 2 mm up the From b9508b9b6a0ad28fc0a405cfb4a287f25d89945d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 15:01:30 +0000 Subject: [PATCH 12/58] Rebuild the pages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 53 ++++++++++++++++++++++++++++++++++++++++++++----- index.html | 53 ++++++++++++++++++++++++++++++++++++++++++++----- sw.js | 2 +- 3 files changed, 97 insertions(+), 11 deletions(-) diff --git a/bins/index.html b/bins/index.html index 331f553..b3453ee 100644 --- a/bins/index.html +++ b/bins/index.html @@ -2423,7 +2423,8 @@

Your drawers

* stand for a size typed (`r`), and back the other way, the largest size whose corners * stay inside a room (`fits`). fastenerCutter and screwCutter cut these and mountLimits * holds the sizes to them, so a bore's shape is decided here and nowhere else. Each is - * cylinder's polygon about the site, a corner at every 2πk/sides from +x. + * cylinder's polygon about the site, a corner at every 2πk/sides from +x, but for a shank + * that `turn` moves (below). * * Every bore stands its flats on the size asked, so its corners are 1/cos(π/sides) of the * radius out and it is no narrower anywhere than the size typed. @@ -2453,7 +2454,31 @@

Your drawers

* pitch, 3.59 over 3.4 at 60, open in the underside, the sliver class that comes and * goes with a size. A head under the line is cut as none, as one no wider than the hole * always was: within 3.5% of the hole it is no seat for a screw head, and all it would - * add is slivers between the shank's corners. */ + * add is slivers between the shank's corners. + * + * Under a counterbore the shank is turned a little about its axis (`turn`, #74) where one + * of the counterbore's corners would stand on one of its flats carried on past the shank. + * The BSP splits what it meets along a flat's whole plane, not just the flat: the floor + * of a magnet pocket from above, which is a ring round the counterbore, or the roof of one + * from beneath round a counterbore deeper than it. A split a micron or two from the + * corner was welded onto it (healCsgSeams), which bent the counterbore's wall off its + * plane, and the plate came out open by the dozen round every site: 136 edges a piece at + * 42 mm for main's 1.0353 mm shank under a 1.0873 head with a 6 x 2 magnet from above, 36 + * a cell for 1.06 over 1 here, 62 for 3.305 over 3, 55 for 5.4959 over 3 under an 8 x 3 + * magnet. Such sizes come every few hundredths at any ratio of head to shank. A corner + * meets a flat's plane at an odd multiple of 15/7° off the flat's normal; turned π/84, + * 15/7°, it meets them at the even ones, so a size that put a corner on a flat puts it + * midway between two. The turn is taken where a corner comes nearer than 0.005 mm + * unturned and the turn leaves it further off, so no corner comes within 0.0039 mm of a + * flat at any size, and a shank no corner came near is cut as it was. Half a facet, π/12, + * clears the corners as well, but it squares two flats to the cell's edges, and at + * 37.67 mm those folded the socket floor beside the pocket. Measured on 3,846 one-cell + * plates with a magnet from above, 37.67 to 55 mm, shanks 1 to 5 mm and heads up to 1.15 + * times them, every 0.001 mm round these sizes: 546 open before, 4 now. On 1,800 random + * designs the turn moves, every kind of mount: 90 open before, 26 now. It closed 83 and + * opened 19, 14 of them at 42 mm with a magnet from beneath under a deeper counterbore, + * where one size in nine leaks unturned too, by the same slivers at the magnet pocket's + * corners in the underside. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, @@ -2462,7 +2487,19 @@

Your drawers

over: (hole) => 2 * MOUNT_BORE.hole.r(hole) + 0.01, cuts: (d, hole) => d >= MOUNT_BORE.head.over(hole) }, hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), - fits: (room) => 2 * room * Math.cos(Math.PI / 12) }, + fits: (room) => 2 * room * Math.cos(Math.PI / 12), + // how far to turn the shank under a counterbore headD across (see above) + turn: (d, headD) => { + const { head, hole } = MOUNT_BORE, R = head.r(headD); + const clear = (t) => { // the nearest a corner of the counterbore comes to a flat's plane + let g = Infinity; + for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) + g = Math.min(g, Math.abs(R * Math.cos(2*Math.PI*k/head.sides - (2*m + 1)*Math.PI/hole.sides - t) - d/2)); + return g; + }; + const now = clear(0); + return now < 0.005 && clear(Math.PI / 84) > now ? Math.PI / 84 : 0; + } }, }; /* Everything removed at one mounting site, as ONE solid. @@ -2501,9 +2538,15 @@

Your drawers

its underside. Left out, the pocket is the same solid, and closed. */ const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; - if (head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet) + const counterbore = head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet; + if (counterbore) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); - add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); + // the shank, turned off the counterbore's corners where one would stand on a flat (#74) + const shank = cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides); + const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD) : 0; + const c = Math.cos(t), s = Math.sin(t), turn = (v) => [v[0]*c - v[1]*s, v[0]*s + v[1]*c, v[2]]; + add(t ? shank.map((p) => ({ verts: p.verts.map(turn), plane: { n: turn(p.plane.n), w: p.plane.w } })) + : shank); } return cut; } diff --git a/index.html b/index.html index 0b3126d..9e5e0e5 100644 --- a/index.html +++ b/index.html @@ -2210,7 +2210,8 @@

Your drawers

* stand for a size typed (`r`), and back the other way, the largest size whose corners * stay inside a room (`fits`). fastenerCutter and screwCutter cut these and mountLimits * holds the sizes to them, so a bore's shape is decided here and nowhere else. Each is - * cylinder's polygon about the site, a corner at every 2πk/sides from +x. + * cylinder's polygon about the site, a corner at every 2πk/sides from +x, but for a shank + * that `turn` moves (below). * * Every bore stands its flats on the size asked, so its corners are 1/cos(π/sides) of the * radius out and it is no narrower anywhere than the size typed. @@ -2240,7 +2241,31 @@

Your drawers

* pitch, 3.59 over 3.4 at 60, open in the underside, the sliver class that comes and * goes with a size. A head under the line is cut as none, as one no wider than the hole * always was: within 3.5% of the hole it is no seat for a screw head, and all it would - * add is slivers between the shank's corners. */ + * add is slivers between the shank's corners. + * + * Under a counterbore the shank is turned a little about its axis (`turn`, #74) where one + * of the counterbore's corners would stand on one of its flats carried on past the shank. + * The BSP splits what it meets along a flat's whole plane, not just the flat: the floor + * of a magnet pocket from above, which is a ring round the counterbore, or the roof of one + * from beneath round a counterbore deeper than it. A split a micron or two from the + * corner was welded onto it (healCsgSeams), which bent the counterbore's wall off its + * plane, and the plate came out open by the dozen round every site: 136 edges a piece at + * 42 mm for main's 1.0353 mm shank under a 1.0873 head with a 6 x 2 magnet from above, 36 + * a cell for 1.06 over 1 here, 62 for 3.305 over 3, 55 for 5.4959 over 3 under an 8 x 3 + * magnet. Such sizes come every few hundredths at any ratio of head to shank. A corner + * meets a flat's plane at an odd multiple of 15/7° off the flat's normal; turned π/84, + * 15/7°, it meets them at the even ones, so a size that put a corner on a flat puts it + * midway between two. The turn is taken where a corner comes nearer than 0.005 mm + * unturned and the turn leaves it further off, so no corner comes within 0.0039 mm of a + * flat at any size, and a shank no corner came near is cut as it was. Half a facet, π/12, + * clears the corners as well, but it squares two flats to the cell's edges, and at + * 37.67 mm those folded the socket floor beside the pocket. Measured on 3,846 one-cell + * plates with a magnet from above, 37.67 to 55 mm, shanks 1 to 5 mm and heads up to 1.15 + * times them, every 0.001 mm round these sizes: 546 open before, 4 now. On 1,800 random + * designs the turn moves, every kind of mount: 90 open before, 26 now. It closed 83 and + * opened 19, 14 of them at 42 mm with a magnet from beneath under a deeper counterbore, + * where one size in nine leaks unturned too, by the same slivers at the magnet pocket's + * corners in the underside. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, @@ -2249,7 +2274,19 @@

Your drawers

over: (hole) => 2 * MOUNT_BORE.hole.r(hole) + 0.01, cuts: (d, hole) => d >= MOUNT_BORE.head.over(hole) }, hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), - fits: (room) => 2 * room * Math.cos(Math.PI / 12) }, + fits: (room) => 2 * room * Math.cos(Math.PI / 12), + // how far to turn the shank under a counterbore headD across (see above) + turn: (d, headD) => { + const { head, hole } = MOUNT_BORE, R = head.r(headD); + const clear = (t) => { // the nearest a corner of the counterbore comes to a flat's plane + let g = Infinity; + for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) + g = Math.min(g, Math.abs(R * Math.cos(2*Math.PI*k/head.sides - (2*m + 1)*Math.PI/hole.sides - t) - d/2)); + return g; + }; + const now = clear(0); + return now < 0.005 && clear(Math.PI / 84) > now ? Math.PI / 84 : 0; + } }, }; /* Everything removed at one mounting site, as ONE solid. @@ -2288,9 +2325,15 @@

Your drawers

its underside. Left out, the pocket is the same solid, and closed. */ const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; - if (head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet) + const counterbore = head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet; + if (counterbore) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); - add(cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides)); + // the shank, turned off the counterbore's corners where one would stand on a flat (#74) + const shank = cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides); + const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD) : 0; + const c = Math.cos(t), s = Math.sin(t), turn = (v) => [v[0]*c - v[1]*s, v[0]*s + v[1]*c, v[2]]; + add(t ? shank.map((p) => ({ verts: p.verts.map(turn), plane: { n: turn(p.plane.n), w: p.plane.w } })) + : shank); } return cut; } diff --git a/sw.js b/sw.js index 33a028c..a182340 100644 --- a/sw.js +++ b/sw.js @@ -52,7 +52,7 @@ load from this site, and the app's icons and manifest, as paths from the root. VERSION is a hash of all of those files, names and bytes, and of this script as it is served, FILES filled in and everything else but VERSION itself. */ -const VERSION = "2d0ca3035d3c"; +const VERSION = "c6f9b690c5d9"; const FILES = [ "./", "bins/", From ef97415a36943fdf0427173eac6c1930e7200fd5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 15:51:49 +0000 Subject: [PATCH 13/58] Keep plates watertight where turning the shank would line a magnet pocket's corner up with one of its flats Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 7 ++++-- src/core.js | 52 +++++++++++++++++++++++++++++---------------- test/plate-audit.js | 8 +++++-- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index d19aa88..8100f4a 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -713,8 +713,11 @@ currently installed): splits a magnet pocket's floor or roof along that whole plane, the weld pulled the corner onto the split, and the plate came out open by the dozen round every site (#74: 136 edges a piece for 1.0353 mm under 1.0873 with a magnet from above at 42 mm, on - main). Turned, no corner comes within 0.0039 mm of a flat at any size, and a shank no - corner came near is cut as before. + main). The magnet pocket's corners stand on the same rays, so where π/84 would bring one + of those within 0.005 mm of a flat instead, the shank takes the step of the 24 across + those 30/7° that keeps both furthest off. Turned, no corner of either comes within + 0.0039 mm of a flat, and a shank no corner of the counterbore's came near is cut as + before. - **The socket's corner clearance is not uniform.** Known, documented, deliberately not fixed — see [socket-clearance.md](socket-clearance.md). - **The plate's outer corner radius has a ceiling, and it is the socket's.** Both the diff --git a/src/core.js b/src/core.js index 081c91c..2cc20ea 100644 --- a/src/core.js +++ b/src/core.js @@ -863,16 +863,25 @@ function cylinder(cx, cy, r, z0, z1, seg) { * meets a flat's plane at an odd multiple of 15/7° off the flat's normal; turned π/84, * 15/7°, it meets them at the even ones, so a size that put a corner on a flat puts it * midway between two. The turn is taken where a corner comes nearer than 0.005 mm - * unturned and the turn leaves it further off, so no corner comes within 0.0039 mm of a - * flat at any size, and a shank no corner came near is cut as it was. Half a facet, π/12, - * clears the corners as well, but it squares two flats to the cell's edges, and at - * 37.67 mm those folded the socket floor beside the pocket. Measured on 3,846 one-cell - * plates with a magnet from above, 37.67 to 55 mm, shanks 1 to 5 mm and heads up to 1.15 - * times them, every 0.001 mm round these sizes: 546 open before, 4 now. On 1,800 random - * designs the turn moves, every kind of mount: 90 open before, 26 now. It closed 83 and - * opened 19, 14 of them at 42 mm with a magnet from beneath under a deeper counterbore, - * where one size in nine leaks unturned too, by the same slivers at the magnet pocket's - * corners in the underside. */ + * unturned and the turn leaves it further off, and a shank no corner came near is cut as + * it was. A magnet pocket's corners stand on the same rays as the counterbore's, and π/84 + * could bring one of those onto a flat instead: 4.85 mm under 7.246 with a 6 mm magnet + * from above left 35 edges in the socket floor at 49.02 mm, and 5.773 under 12.053 25 at + * 55 mm, where the flats unturned miss them by 0.07 and 0.04 (unturned shanks meet them at + * other sizes: 4.9911 under 7.246 leaves 44 at 49.02 mm, with no turn). So where π/84 + * leaves a corner of either nearer than 0.005 mm, the turn is the one of 24 steps across + * those 30/7° that keeps them furthest off. Wherever the counterbore's corners call for a + * turn, no corner of either then comes within 0.0039 mm of a flat (6 and 8 mm magnets, + * heads up to 2.2 times the shank). Half a facet, π/12, clears the corners as well, but + * it squares two flats to the cell's edges, and at 37.67 mm those folded the socket floor + * beside the pocket. Measured on 3,846 one-cell plates with a magnet from above, 37.67 to + * 55 mm, shanks 1 to 5 mm and heads up to 1.15 times them, every 0.001 mm round these + * sizes: 546 open before, and 1 now, a size the turn leaves alone; none came out worse. + * On 1,800 random designs the turn moves, every kind of mount: 90 open before, 24 now. It + * closed 83 and opened 17: 14 at 42 mm with a magnet from beneath under a deeper + * counterbore, where one size in nine leaks unturned too, by the same slivers at the + * magnet pocket's corners in the underside, and 3 in corner bosses, each at that one size + * alone. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, @@ -882,17 +891,24 @@ const MOUNT_BORE = { cuts: (d, hole) => d >= MOUNT_BORE.head.over(hole) }, hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), fits: (room) => 2 * room * Math.cos(Math.PI / 12), - // how far to turn the shank under a counterbore headD across (see above) - turn: (d, headD) => { - const { head, hole } = MOUNT_BORE, R = head.r(headD); - const clear = (t) => { // the nearest a corner of the counterbore comes to a flat's plane + // how far to turn the shank under a counterbore headD across, in a magnet pocket + // magnetR out at its corners or none (see above) + turn: (d, headD, magnetR) => { + const { head, hole } = MOUNT_BORE, rs = [head.r(headD)]; + const clear = (t) => { // the nearest a corner comes to one of the flats' planes let g = Infinity; - for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) + for (const R of rs) for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) g = Math.min(g, Math.abs(R * Math.cos(2*Math.PI*k/head.sides - (2*m + 1)*Math.PI/hole.sides - t) - d/2)); return g; }; - const now = clear(0); - return now < 0.005 && clear(Math.PI / 84) > now ? Math.PI / 84 : 0; + if (clear(0) >= 0.005) return 0; + if (magnetR) rs.push(magnetR); // the magnet pocket's corners stand on the same rays + let best = Math.PI / 84, most = clear(best); + if (most < 0.005) for (let j = 1; j < 24; j++) { + const t = (Math.PI / 42) * j / 24, g = clear(t); + if (g > most + 1e-9) { most = g; best = t; } + } + return most > clear(0) ? best : 0; } }, }; @@ -937,7 +953,7 @@ function fastenerCutter(cfg, magZ0, magZ1, shankTop) { add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); // the shank, turned off the counterbore's corners where one would stand on a flat (#74) const shank = cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides); - const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD) : 0; + const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD, cfg.magnets ? magnetR : 0) : 0; const c = Math.cos(t), s = Math.sin(t), turn = (v) => [v[0]*c - v[1]*s, v[0]*s + v[1]*c, v[2]]; add(t ? shank.map((p) => ({ verts: p.verts.map(turn), plane: { n: turn(p.plane.n), w: p.plane.w } })) : shank); diff --git a/test/plate-audit.js b/test/plate-audit.js index bec4933..b2a972f 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -2055,8 +2055,10 @@ console.log('\nthe other limits, built at their ends:'); flats on the sizes, are a 1 mm shank under 1.06 (36 a cell). 4.25 over 4 left 26 and 3.305 over 3 62; 5.4959 over 3 under an 8 x 3 magnet, a head nearly twice the shank, 55; and from beneath, the magnet's roof round a counterbore deeper than it, 1.06 over - 1 left 12. The shank is turned off the corners now (MOUNT_BORE.hole.turn). Each has - to build closed, with no folds. */ + 1 left 12. The shank is turned off the corners now (MOUNT_BORE.hole.turn). The last + two are where π/84 alone put a corner of the magnet pocket's on a flat instead (35 and + 25 edges in the socket floor), so the turn keeps those clear too. Each has to build + closed, with no folds. */ for (const [nm, o] of [ ['main\'s 1.0353 under 1.0873, 42', { pitch: 42, drawerW: 168, drawerD: 84, bedW: 100, bedD: 400, screwHoleD: 1.0353 * Math.cos(Math.PI / 12), screwHeadD: 1.0873 * Math.cos(Math.PI / 14) }], @@ -2065,6 +2067,8 @@ console.log('\nthe other limits, built at their ends:'); ['3.305 mm head over 3, 42', { screwHoleD: 3, screwHeadD: 3.305 }], ['5.4959 over 3, 8 x 3 magnet', { screwHoleD: 3, screwHeadD: 5.4959, magnetD: 8, magnetH: 3 }], ['1.06 over 1, from beneath', { screwHoleD: 1, screwHeadD: 1.06, magnetSide: 'bottom', screwHeadDepth: 3 }], + ['4.85 under 7.246, 49.02', { pitch: 49.02, drawerW: 49.02, drawerD: 49.02, screwHoleD: 4.85, screwHeadD: 7.246 }], + ['5.773 under 12.053, 55', { pitch: 55, drawerW: 55, drawerD: 55, screwHoleD: 5.773, screwHeadD: 12.053 }], ]) { const r = buildAll({ pitch: 42, drawerW: 42, drawerD: 42, connector: 'none', magnets: true, magnetSide: 'top', screws: true, ...o }); From 705b8fb8d4000463a482ff34e94ae1c79b2a3644 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 15:51:50 +0000 Subject: [PATCH 14/58] Rebuild the pages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 52 ++++++++++++++++++++++++++++++++----------------- index.html | 52 ++++++++++++++++++++++++++++++++----------------- sw.js | 2 +- 3 files changed, 69 insertions(+), 37 deletions(-) diff --git a/bins/index.html b/bins/index.html index b3453ee..ceee5ad 100644 --- a/bins/index.html +++ b/bins/index.html @@ -2469,16 +2469,25 @@

Your drawers

* meets a flat's plane at an odd multiple of 15/7° off the flat's normal; turned π/84, * 15/7°, it meets them at the even ones, so a size that put a corner on a flat puts it * midway between two. The turn is taken where a corner comes nearer than 0.005 mm - * unturned and the turn leaves it further off, so no corner comes within 0.0039 mm of a - * flat at any size, and a shank no corner came near is cut as it was. Half a facet, π/12, - * clears the corners as well, but it squares two flats to the cell's edges, and at - * 37.67 mm those folded the socket floor beside the pocket. Measured on 3,846 one-cell - * plates with a magnet from above, 37.67 to 55 mm, shanks 1 to 5 mm and heads up to 1.15 - * times them, every 0.001 mm round these sizes: 546 open before, 4 now. On 1,800 random - * designs the turn moves, every kind of mount: 90 open before, 26 now. It closed 83 and - * opened 19, 14 of them at 42 mm with a magnet from beneath under a deeper counterbore, - * where one size in nine leaks unturned too, by the same slivers at the magnet pocket's - * corners in the underside. */ + * unturned and the turn leaves it further off, and a shank no corner came near is cut as + * it was. A magnet pocket's corners stand on the same rays as the counterbore's, and π/84 + * could bring one of those onto a flat instead: 4.85 mm under 7.246 with a 6 mm magnet + * from above left 35 edges in the socket floor at 49.02 mm, and 5.773 under 12.053 25 at + * 55 mm, where the flats unturned miss them by 0.07 and 0.04 (unturned shanks meet them at + * other sizes: 4.9911 under 7.246 leaves 44 at 49.02 mm, with no turn). So where π/84 + * leaves a corner of either nearer than 0.005 mm, the turn is the one of 24 steps across + * those 30/7° that keeps them furthest off. Wherever the counterbore's corners call for a + * turn, no corner of either then comes within 0.0039 mm of a flat (6 and 8 mm magnets, + * heads up to 2.2 times the shank). Half a facet, π/12, clears the corners as well, but + * it squares two flats to the cell's edges, and at 37.67 mm those folded the socket floor + * beside the pocket. Measured on 3,846 one-cell plates with a magnet from above, 37.67 to + * 55 mm, shanks 1 to 5 mm and heads up to 1.15 times them, every 0.001 mm round these + * sizes: 546 open before, and 1 now, a size the turn leaves alone; none came out worse. + * On 1,800 random designs the turn moves, every kind of mount: 90 open before, 24 now. It + * closed 83 and opened 17: 14 at 42 mm with a magnet from beneath under a deeper + * counterbore, where one size in nine leaks unturned too, by the same slivers at the + * magnet pocket's corners in the underside, and 3 in corner bosses, each at that one size + * alone. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, @@ -2488,17 +2497,24 @@

Your drawers

cuts: (d, hole) => d >= MOUNT_BORE.head.over(hole) }, hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), fits: (room) => 2 * room * Math.cos(Math.PI / 12), - // how far to turn the shank under a counterbore headD across (see above) - turn: (d, headD) => { - const { head, hole } = MOUNT_BORE, R = head.r(headD); - const clear = (t) => { // the nearest a corner of the counterbore comes to a flat's plane + // how far to turn the shank under a counterbore headD across, in a magnet pocket + // magnetR out at its corners or none (see above) + turn: (d, headD, magnetR) => { + const { head, hole } = MOUNT_BORE, rs = [head.r(headD)]; + const clear = (t) => { // the nearest a corner comes to one of the flats' planes let g = Infinity; - for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) + for (const R of rs) for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) g = Math.min(g, Math.abs(R * Math.cos(2*Math.PI*k/head.sides - (2*m + 1)*Math.PI/hole.sides - t) - d/2)); return g; }; - const now = clear(0); - return now < 0.005 && clear(Math.PI / 84) > now ? Math.PI / 84 : 0; + if (clear(0) >= 0.005) return 0; + if (magnetR) rs.push(magnetR); // the magnet pocket's corners stand on the same rays + let best = Math.PI / 84, most = clear(best); + if (most < 0.005) for (let j = 1; j < 24; j++) { + const t = (Math.PI / 42) * j / 24, g = clear(t); + if (g > most + 1e-9) { most = g; best = t; } + } + return most > clear(0) ? best : 0; } }, }; @@ -2543,7 +2559,7 @@

Your drawers

add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); // the shank, turned off the counterbore's corners where one would stand on a flat (#74) const shank = cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides); - const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD) : 0; + const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD, cfg.magnets ? magnetR : 0) : 0; const c = Math.cos(t), s = Math.sin(t), turn = (v) => [v[0]*c - v[1]*s, v[0]*s + v[1]*c, v[2]]; add(t ? shank.map((p) => ({ verts: p.verts.map(turn), plane: { n: turn(p.plane.n), w: p.plane.w } })) : shank); diff --git a/index.html b/index.html index 9e5e0e5..d1040bd 100644 --- a/index.html +++ b/index.html @@ -2256,16 +2256,25 @@

Your drawers

* meets a flat's plane at an odd multiple of 15/7° off the flat's normal; turned π/84, * 15/7°, it meets them at the even ones, so a size that put a corner on a flat puts it * midway between two. The turn is taken where a corner comes nearer than 0.005 mm - * unturned and the turn leaves it further off, so no corner comes within 0.0039 mm of a - * flat at any size, and a shank no corner came near is cut as it was. Half a facet, π/12, - * clears the corners as well, but it squares two flats to the cell's edges, and at - * 37.67 mm those folded the socket floor beside the pocket. Measured on 3,846 one-cell - * plates with a magnet from above, 37.67 to 55 mm, shanks 1 to 5 mm and heads up to 1.15 - * times them, every 0.001 mm round these sizes: 546 open before, 4 now. On 1,800 random - * designs the turn moves, every kind of mount: 90 open before, 26 now. It closed 83 and - * opened 19, 14 of them at 42 mm with a magnet from beneath under a deeper counterbore, - * where one size in nine leaks unturned too, by the same slivers at the magnet pocket's - * corners in the underside. */ + * unturned and the turn leaves it further off, and a shank no corner came near is cut as + * it was. A magnet pocket's corners stand on the same rays as the counterbore's, and π/84 + * could bring one of those onto a flat instead: 4.85 mm under 7.246 with a 6 mm magnet + * from above left 35 edges in the socket floor at 49.02 mm, and 5.773 under 12.053 25 at + * 55 mm, where the flats unturned miss them by 0.07 and 0.04 (unturned shanks meet them at + * other sizes: 4.9911 under 7.246 leaves 44 at 49.02 mm, with no turn). So where π/84 + * leaves a corner of either nearer than 0.005 mm, the turn is the one of 24 steps across + * those 30/7° that keeps them furthest off. Wherever the counterbore's corners call for a + * turn, no corner of either then comes within 0.0039 mm of a flat (6 and 8 mm magnets, + * heads up to 2.2 times the shank). Half a facet, π/12, clears the corners as well, but + * it squares two flats to the cell's edges, and at 37.67 mm those folded the socket floor + * beside the pocket. Measured on 3,846 one-cell plates with a magnet from above, 37.67 to + * 55 mm, shanks 1 to 5 mm and heads up to 1.15 times them, every 0.001 mm round these + * sizes: 546 open before, and 1 now, a size the turn leaves alone; none came out worse. + * On 1,800 random designs the turn moves, every kind of mount: 90 open before, 24 now. It + * closed 83 and opened 17: 14 at 42 mm with a magnet from beneath under a deeper + * counterbore, where one size in nine leaks unturned too, by the same slivers at the + * magnet pocket's corners in the underside, and 3 in corner bosses, each at that one size + * alone. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, @@ -2275,17 +2284,24 @@

Your drawers

cuts: (d, hole) => d >= MOUNT_BORE.head.over(hole) }, hole: { sides: 12, r: (d) => d/2 / Math.cos(Math.PI/12), fits: (room) => 2 * room * Math.cos(Math.PI / 12), - // how far to turn the shank under a counterbore headD across (see above) - turn: (d, headD) => { - const { head, hole } = MOUNT_BORE, R = head.r(headD); - const clear = (t) => { // the nearest a corner of the counterbore comes to a flat's plane + // how far to turn the shank under a counterbore headD across, in a magnet pocket + // magnetR out at its corners or none (see above) + turn: (d, headD, magnetR) => { + const { head, hole } = MOUNT_BORE, rs = [head.r(headD)]; + const clear = (t) => { // the nearest a corner comes to one of the flats' planes let g = Infinity; - for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) + for (const R of rs) for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) g = Math.min(g, Math.abs(R * Math.cos(2*Math.PI*k/head.sides - (2*m + 1)*Math.PI/hole.sides - t) - d/2)); return g; }; - const now = clear(0); - return now < 0.005 && clear(Math.PI / 84) > now ? Math.PI / 84 : 0; + if (clear(0) >= 0.005) return 0; + if (magnetR) rs.push(magnetR); // the magnet pocket's corners stand on the same rays + let best = Math.PI / 84, most = clear(best); + if (most < 0.005) for (let j = 1; j < 24; j++) { + const t = (Math.PI / 42) * j / 24, g = clear(t); + if (g > most + 1e-9) { most = g; best = t; } + } + return most > clear(0) ? best : 0; } }, }; @@ -2330,7 +2346,7 @@

Your drawers

add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); // the shank, turned off the counterbore's corners where one would stand on a flat (#74) const shank = cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides); - const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD) : 0; + const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD, cfg.magnets ? magnetR : 0) : 0; const c = Math.cos(t), s = Math.sin(t), turn = (v) => [v[0]*c - v[1]*s, v[0]*s + v[1]*c, v[2]]; add(t ? shank.map((p) => ({ verts: p.verts.map(turn), plane: { n: turn(p.plane.n), w: p.plane.w } })) : shank); diff --git a/sw.js b/sw.js index a182340..8984595 100644 --- a/sw.js +++ b/sw.js @@ -52,7 +52,7 @@ load from this site, and the app's icons and manifest, as paths from the root. VERSION is a hash of all of those files, names and bytes, and of this script as it is served, FILES filled in and everything else but VERSION itself. */ -const VERSION = "c6f9b690c5d9"; +const VERSION = "4c17bd60043c"; const FILES = [ "./", "bins/", From a9a445c1dde5d958300c4cbc89cfbff7ebb019af Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 16:31:29 +0000 Subject: [PATCH 15/58] Measure a screw's shank turned, as it is cut, where a joint's cut beside it sets its size Where a counterbore's corner would stand on a flat of its shank, fastenerCutter turns the shank a little (#74), and a turned shank reaches a joint's cut a little sooner or later than one that is not. MOUNT_BORE.screw now says, for the cutter and for mountLimits alike, whether the counterbore is cut and how far the shank is turned, and mountLimits measures that shank turned (boreReach takes the turn). On 400 random designs with a turned shank its limit moved by a tenth in 2; no design whose head and magnet the limits also take changed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 4 ++- src/core.js | 67 +++++++++++++++++++++++++++++++------------------- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 67ad889..8da14c6 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -717,7 +717,9 @@ currently installed): rays, so where π/84 would bring one of those within 0.005 mm of a flat instead, the shank takes the step of the 24 across those 30/7° that keeps both furthest off. Turned, no corner of either comes within 0.0039 mm of a flat, and a shank no corner of the - counterbore's came near is cut as before. + counterbore's came near is cut as before. `MOUNT_BORE.screw` says whether a design's + counterbore is cut and how far its shank is turned, for the cutter and for + `mountLimits`, which measures that shank turned. - **The socket's corner clearance is not uniform.** Known, documented, deliberately not fixed — see [socket-clearance.md](socket-clearance.md). - **The plate's outer corner radius has a ceiling, and it is the socket's.** Both the diff --git a/src/core.js b/src/core.js index adc2903..6d45497 100644 --- a/src/core.js +++ b/src/core.js @@ -910,6 +910,22 @@ const MOUNT_BORE = { } return most > clear(0) ? best : 0; } }, + /* What fastenerCutter cuts for a design's screw, as { counterbore, turn }: whether the + counterbore is cut, and how far the shank is turned under it. Here so that mountLimits + measures the same shank the cutter cuts. + A counterbore the magnet pocket from beneath holds whole is that pocket already: both + 14 sides, corners on the same rays, no deeper. Since the bores stand their flats on + the sizes, the default 6 mm head's walls stand 0.022 mm inside the 6 mm magnet's, + and unioned, the test tile at 42 mm with magnets and screws left 6 edges open in + its underside. Left out, the pocket is the same solid, and closed. */ + screw: (cfg) => { + const { magnet, head, hole } = MOUNT_BORE, magnetR = magnet.r(cfg.magnetD); + const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && + head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; + const counterbore = head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet; + return { counterbore, + turn: counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD, cfg.magnets ? magnetR : 0) : 0 }; + }, }; /* Everything removed at one mounting site, as ONE solid. @@ -941,19 +957,12 @@ function fastenerCutter(cfg, magZ0, magZ1, shankTop) { ? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { - /* A counterbore the magnet pocket from beneath holds whole is that pocket already: both - 14 sides, corners on the same rays, no deeper. Since the bores stand their flats on - the sizes, the default 6 mm head's walls stand 0.022 mm inside the 6 mm magnet's, - and unioned, the test tile at 42 mm with magnets and screws left 6 edges open in - its underside. Left out, the pocket is the same solid, and closed. */ - const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && - head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; - const counterbore = head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet; + // the counterbore, unless the magnet pocket from beneath is that already (MOUNT_BORE.screw) + const { counterbore, turn: t } = MOUNT_BORE.screw(cfg); if (counterbore) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); // the shank, turned off the counterbore's corners where one would stand on a flat (#74) const shank = cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides); - const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD, cfg.magnets ? magnetR : 0) : 0; const c = Math.cos(t), s = Math.sin(t), turn = (v) => [v[0]*c - v[1]*s, v[0]*s + v[1]*c, v[2]]; add(t ? shank.map((p) => ({ verts: p.verts.map(turn), plane: { n: turn(p.plane.n), w: p.plane.w } })) : shank); @@ -2464,16 +2473,17 @@ const MOUNT_WALL = 1.0; const MOUNT_LEVEL = 0.05, MOUNT_SEAM = 0.01; /* How far a mounting bore about (x, y) can grow before it meets any of `segs`, as the radius of the circle through its corners: the polygon cylinder() cuts, `sides` flats - with a corner at every 2πk/sides from +x. A point is in that polygon when it stands no - further out along any flat's normal than R cos(π/sides), so along one segment the - least R that takes a point of it in is at an end, or where the segment crosses the - line out through a corner, on which a point's R is its distance. */ -function boreReach(sides, segs, x, y) { + with a corner at every 2πk/sides from +x, or that turned `turn` about its axis, as + fastenerCutter turns a shank (MOUNT_BORE.screw). A point is in that polygon when it + stands no further out along any flat's normal than R cos(π/sides), so along one + segment the least R that takes a point of it in is at an end, or where the segment + crosses the line out through a corner, on which a point's R is its distance. */ +function boreReach(sides, segs, x, y, turn = 0) { const step = 2 * Math.PI / sides, h = Math.cos(step / 2); const cx = [], cy = [], nx = [], ny = []; for (let k = 0; k < sides; k++) { - cx.push(Math.cos(k * step)); cy.push(Math.sin(k * step)); - nx.push(Math.cos((k + 0.5) * step)); ny.push(Math.sin((k + 0.5) * step)); + cx.push(Math.cos(k * step + turn)); cy.push(Math.sin(k * step + turn)); + nx.push(Math.cos((k + 0.5) * step + turn)); ny.push(Math.sin((k + 0.5) * step + turn)); } const at = (dx, dy) => { let m = -Infinity; @@ -2609,10 +2619,11 @@ function mountLimits(cfg, layout, known) { magnet from beneath with a bowtie at 42 mm, which has 0.84 mm to spare and always built clean. */ const pad = platePad(cfg), again = !!known && known.pad === pad; - /* Each cut as { top, sites }: its ceiling, and each site in reach of it as how far a - bore's corner circle (`room`) and the bores themselves (`bore`, by their sides) can - grow before they meet the cut, and a magnet or screw (`body`) before it meets the - part in it. */ + /* Each cut as { top, segs, sites }: its ceiling, its footprint, and each site in reach + of it as how far a bore's corner circle (`room`) and the bores themselves (`bore`, by + their sides) can grow before they meet the cut, and a magnet or screw (`body`) before + it meets the part in it. Where the site stands from the cut's own origin (`x`, `y`) + is kept for a shank turned under its counterbore, measured for the sizes asked. */ const jointCuts = again ? known.list : []; /* Room past the most the cell or the floor leaves cannot stop a size, so nothing further off than that is measured, and nothing at all where neither leaves any. A @@ -2689,17 +2700,20 @@ function mountLimits(cfg, layout, known) { const room = from(segs, x, y); if (room >= reach) continue; const bore = (n) => room > 0 ? boreReach(n, segs, x, y) : room; - sites.push({ room, body: from(part, x, y), bore: { 12: bore(12), 14: bore(14) } }); + sites.push({ x, y, room, body: from(part, x, y), bore: { 12: bore(12), 14: bore(14) } }); } - if (sites.length) jointCuts.push({ top: ceiling, sites }); + if (sites.length) jointCuts.push({ top: ceiling, segs, sites }); } } } /* The bores as fastenerCutter cuts them (MOUNT_BORE): how many sides, and the largest size whose corners stay inside a radius. Each stands its flats on its size, so its corners stand out past it, and a magnet's 0.1 mm over its radius where that is - further. */ + further. The shank is turned where the cutter turns it, and measured so: its turn + goes by the sizes asked, the head's and the magnet's as well as its own, so it is + the shank of this design that is measured. */ const BORE = { magnetD: MOUNT_BORE.magnet, screwHeadD: MOUNT_BORE.head, screwHoleD: MOUNT_BORE.hole }; + const shankTurn = cfg.screws ? MOUNT_BORE.screw(cfg).turn : 0; // the room the cell, the floor or a boss leaves, and the hole beside const own = { magnetD: fit(cfg.magnetSide === 'top' ? top : under), screwHoleD: fit(top), screwHeadD: fit(under) }; // a dovetail's notch takes a pocket from beneath that breaks into it; every other cut does not @@ -2713,6 +2727,9 @@ function mountLimits(cfg, layout, known) { into, but not stop just short of. */ const jointFor = (f) => { const { sides, fits } = BORE[f]; + // the room a bore has, measured again for a shank turned under its counterbore + const turn = f === 'screwHoleD' ? shankTurn : 0; + const reach = (c, s) => !turn ? s.bore[sides] : s.room > 0 ? boreReach(sides, c.segs, s.x, s.y, turn) : s.room; const above = f === 'magnetD' && cfg.magnetSide === 'top'; // how high a pocket from beneath goes; a shank goes through const ceiling = f === 'magnetD' ? cfg.magnetH : f === 'screwHeadD' ? cfg.screwHeadDepth : Infinity; @@ -2728,10 +2745,10 @@ function mountLimits(cfg, layout, known) { const level = Math.round(Math.abs(ceiling - c.top) * 1e6) / 1e6 < MOUNT_LEVEL; const stop = above ? 'floor' : !notch ? 'cut' : level ? 'level' : 'part'; for (const s of c.sites) { - const short = fits(s.bore[sides] - MOUNT_SEAM); + const bore = reach(c, s), short = fits(bore - MOUNT_SEAM); const size = stop === 'part' ? 2 * s.body : short; if (size < cap) { cap = size; why = stop; } - if (stop === 'part') near.push([short, fits(s.bore[sides])]); + if (stop === 'part') near.push([short, fits(bore)]); } } return { cap, why, near }; From 0e12f68cf08e3db6cfd68ba3e35f22e5433dd3a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 16:31:29 +0000 Subject: [PATCH 16/58] Rebuild the pages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 67 +++++++++++++++++++++++++++++++------------------ index.html | 67 +++++++++++++++++++++++++++++++------------------ sw.js | 2 +- 3 files changed, 85 insertions(+), 51 deletions(-) diff --git a/bins/index.html b/bins/index.html index 418f362..e2f5e6b 100644 --- a/bins/index.html +++ b/bins/index.html @@ -2516,6 +2516,22 @@

Your drawers

} return most > clear(0) ? best : 0; } }, + /* What fastenerCutter cuts for a design's screw, as { counterbore, turn }: whether the + counterbore is cut, and how far the shank is turned under it. Here so that mountLimits + measures the same shank the cutter cuts. + A counterbore the magnet pocket from beneath holds whole is that pocket already: both + 14 sides, corners on the same rays, no deeper. Since the bores stand their flats on + the sizes, the default 6 mm head's walls stand 0.022 mm inside the 6 mm magnet's, + and unioned, the test tile at 42 mm with magnets and screws left 6 edges open in + its underside. Left out, the pocket is the same solid, and closed. */ + screw: (cfg) => { + const { magnet, head, hole } = MOUNT_BORE, magnetR = magnet.r(cfg.magnetD); + const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && + head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; + const counterbore = head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet; + return { counterbore, + turn: counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD, cfg.magnets ? magnetR : 0) : 0 }; + }, }; /* Everything removed at one mounting site, as ONE solid. @@ -2547,19 +2563,12 @@

Your drawers

? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { - /* A counterbore the magnet pocket from beneath holds whole is that pocket already: both - 14 sides, corners on the same rays, no deeper. Since the bores stand their flats on - the sizes, the default 6 mm head's walls stand 0.022 mm inside the 6 mm magnet's, - and unioned, the test tile at 42 mm with magnets and screws left 6 edges open in - its underside. Left out, the pocket is the same solid, and closed. */ - const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && - head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; - const counterbore = head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet; + // the counterbore, unless the magnet pocket from beneath is that already (MOUNT_BORE.screw) + const { counterbore, turn: t } = MOUNT_BORE.screw(cfg); if (counterbore) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); // the shank, turned off the counterbore's corners where one would stand on a flat (#74) const shank = cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides); - const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD, cfg.magnets ? magnetR : 0) : 0; const c = Math.cos(t), s = Math.sin(t), turn = (v) => [v[0]*c - v[1]*s, v[0]*s + v[1]*c, v[2]]; add(t ? shank.map((p) => ({ verts: p.verts.map(turn), plane: { n: turn(p.plane.n), w: p.plane.w } })) : shank); @@ -4070,16 +4079,17 @@

Your drawers

const MOUNT_LEVEL = 0.05, MOUNT_SEAM = 0.01; /* How far a mounting bore about (x, y) can grow before it meets any of `segs`, as the radius of the circle through its corners: the polygon cylinder() cuts, `sides` flats - with a corner at every 2πk/sides from +x. A point is in that polygon when it stands no - further out along any flat's normal than R cos(π/sides), so along one segment the - least R that takes a point of it in is at an end, or where the segment crosses the - line out through a corner, on which a point's R is its distance. */ -function boreReach(sides, segs, x, y) { + with a corner at every 2πk/sides from +x, or that turned `turn` about its axis, as + fastenerCutter turns a shank (MOUNT_BORE.screw). A point is in that polygon when it + stands no further out along any flat's normal than R cos(π/sides), so along one + segment the least R that takes a point of it in is at an end, or where the segment + crosses the line out through a corner, on which a point's R is its distance. */ +function boreReach(sides, segs, x, y, turn = 0) { const step = 2 * Math.PI / sides, h = Math.cos(step / 2); const cx = [], cy = [], nx = [], ny = []; for (let k = 0; k < sides; k++) { - cx.push(Math.cos(k * step)); cy.push(Math.sin(k * step)); - nx.push(Math.cos((k + 0.5) * step)); ny.push(Math.sin((k + 0.5) * step)); + cx.push(Math.cos(k * step + turn)); cy.push(Math.sin(k * step + turn)); + nx.push(Math.cos((k + 0.5) * step + turn)); ny.push(Math.sin((k + 0.5) * step + turn)); } const at = (dx, dy) => { let m = -Infinity; @@ -4215,10 +4225,11 @@

Your drawers

magnet from beneath with a bowtie at 42 mm, which has 0.84 mm to spare and always built clean. */ const pad = platePad(cfg), again = !!known && known.pad === pad; - /* Each cut as { top, sites }: its ceiling, and each site in reach of it as how far a - bore's corner circle (`room`) and the bores themselves (`bore`, by their sides) can - grow before they meet the cut, and a magnet or screw (`body`) before it meets the - part in it. */ + /* Each cut as { top, segs, sites }: its ceiling, its footprint, and each site in reach + of it as how far a bore's corner circle (`room`) and the bores themselves (`bore`, by + their sides) can grow before they meet the cut, and a magnet or screw (`body`) before + it meets the part in it. Where the site stands from the cut's own origin (`x`, `y`) + is kept for a shank turned under its counterbore, measured for the sizes asked. */ const jointCuts = again ? known.list : []; /* Room past the most the cell or the floor leaves cannot stop a size, so nothing further off than that is measured, and nothing at all where neither leaves any. A @@ -4295,17 +4306,20 @@

Your drawers

const room = from(segs, x, y); if (room >= reach) continue; const bore = (n) => room > 0 ? boreReach(n, segs, x, y) : room; - sites.push({ room, body: from(part, x, y), bore: { 12: bore(12), 14: bore(14) } }); + sites.push({ x, y, room, body: from(part, x, y), bore: { 12: bore(12), 14: bore(14) } }); } - if (sites.length) jointCuts.push({ top: ceiling, sites }); + if (sites.length) jointCuts.push({ top: ceiling, segs, sites }); } } } /* The bores as fastenerCutter cuts them (MOUNT_BORE): how many sides, and the largest size whose corners stay inside a radius. Each stands its flats on its size, so its corners stand out past it, and a magnet's 0.1 mm over its radius where that is - further. */ + further. The shank is turned where the cutter turns it, and measured so: its turn + goes by the sizes asked, the head's and the magnet's as well as its own, so it is + the shank of this design that is measured. */ const BORE = { magnetD: MOUNT_BORE.magnet, screwHeadD: MOUNT_BORE.head, screwHoleD: MOUNT_BORE.hole }; + const shankTurn = cfg.screws ? MOUNT_BORE.screw(cfg).turn : 0; // the room the cell, the floor or a boss leaves, and the hole beside const own = { magnetD: fit(cfg.magnetSide === 'top' ? top : under), screwHoleD: fit(top), screwHeadD: fit(under) }; // a dovetail's notch takes a pocket from beneath that breaks into it; every other cut does not @@ -4319,6 +4333,9 @@

Your drawers

into, but not stop just short of. */ const jointFor = (f) => { const { sides, fits } = BORE[f]; + // the room a bore has, measured again for a shank turned under its counterbore + const turn = f === 'screwHoleD' ? shankTurn : 0; + const reach = (c, s) => !turn ? s.bore[sides] : s.room > 0 ? boreReach(sides, c.segs, s.x, s.y, turn) : s.room; const above = f === 'magnetD' && cfg.magnetSide === 'top'; // how high a pocket from beneath goes; a shank goes through const ceiling = f === 'magnetD' ? cfg.magnetH : f === 'screwHeadD' ? cfg.screwHeadDepth : Infinity; @@ -4334,10 +4351,10 @@

Your drawers

const level = Math.round(Math.abs(ceiling - c.top) * 1e6) / 1e6 < MOUNT_LEVEL; const stop = above ? 'floor' : !notch ? 'cut' : level ? 'level' : 'part'; for (const s of c.sites) { - const short = fits(s.bore[sides] - MOUNT_SEAM); + const bore = reach(c, s), short = fits(bore - MOUNT_SEAM); const size = stop === 'part' ? 2 * s.body : short; if (size < cap) { cap = size; why = stop; } - if (stop === 'part') near.push([short, fits(s.bore[sides])]); + if (stop === 'part') near.push([short, fits(bore)]); } } return { cap, why, near }; diff --git a/index.html b/index.html index 5c60ffe..bb05ebb 100644 --- a/index.html +++ b/index.html @@ -2303,6 +2303,22 @@

Your drawers

} return most > clear(0) ? best : 0; } }, + /* What fastenerCutter cuts for a design's screw, as { counterbore, turn }: whether the + counterbore is cut, and how far the shank is turned under it. Here so that mountLimits + measures the same shank the cutter cuts. + A counterbore the magnet pocket from beneath holds whole is that pocket already: both + 14 sides, corners on the same rays, no deeper. Since the bores stand their flats on + the sizes, the default 6 mm head's walls stand 0.022 mm inside the 6 mm magnet's, + and unioned, the test tile at 42 mm with magnets and screws left 6 edges open in + its underside. Left out, the pocket is the same solid, and closed. */ + screw: (cfg) => { + const { magnet, head, hole } = MOUNT_BORE, magnetR = magnet.r(cfg.magnetD); + const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && + head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; + const counterbore = head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet; + return { counterbore, + turn: counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD, cfg.magnets ? magnetR : 0) : 0 }; + }, }; /* Everything removed at one mounting site, as ONE solid. @@ -2334,19 +2350,12 @@

Your drawers

? cylinder(0, 0, magnetR, magZ0, magZ1, magnet.sides) : cylinder(0, 0, magnetR, -0.5, cfg.magnetH, magnet.sides)); if (cfg.screws) { - /* A counterbore the magnet pocket from beneath holds whole is that pocket already: both - 14 sides, corners on the same rays, no deeper. Since the bores stand their flats on - the sizes, the default 6 mm head's walls stand 0.022 mm inside the 6 mm magnet's, - and unioned, the test tile at 42 mm with magnets and screws left 6 edges open in - its underside. Left out, the pocket is the same solid, and closed. */ - const inMagnet = cfg.magnets && cfg.magnetSide !== 'top' && - head.r(cfg.screwHeadD) <= magnetR && cfg.screwHeadDepth <= cfg.magnetH; - const counterbore = head.cuts(cfg.screwHeadD, cfg.screwHoleD) && !inMagnet; + // the counterbore, unless the magnet pocket from beneath is that already (MOUNT_BORE.screw) + const { counterbore, turn: t } = MOUNT_BORE.screw(cfg); if (counterbore) add(cylinder(0, 0, head.r(cfg.screwHeadD), -0.5, cfg.screwHeadDepth, head.sides)); // the shank, turned off the counterbore's corners where one would stand on a flat (#74) const shank = cylinder(0, 0, hole.r(cfg.screwHoleD), -0.5, shankTop, hole.sides); - const t = counterbore ? hole.turn(cfg.screwHoleD, cfg.screwHeadD, cfg.magnets ? magnetR : 0) : 0; const c = Math.cos(t), s = Math.sin(t), turn = (v) => [v[0]*c - v[1]*s, v[0]*s + v[1]*c, v[2]]; add(t ? shank.map((p) => ({ verts: p.verts.map(turn), plane: { n: turn(p.plane.n), w: p.plane.w } })) : shank); @@ -3857,16 +3866,17 @@

Your drawers

const MOUNT_LEVEL = 0.05, MOUNT_SEAM = 0.01; /* How far a mounting bore about (x, y) can grow before it meets any of `segs`, as the radius of the circle through its corners: the polygon cylinder() cuts, `sides` flats - with a corner at every 2πk/sides from +x. A point is in that polygon when it stands no - further out along any flat's normal than R cos(π/sides), so along one segment the - least R that takes a point of it in is at an end, or where the segment crosses the - line out through a corner, on which a point's R is its distance. */ -function boreReach(sides, segs, x, y) { + with a corner at every 2πk/sides from +x, or that turned `turn` about its axis, as + fastenerCutter turns a shank (MOUNT_BORE.screw). A point is in that polygon when it + stands no further out along any flat's normal than R cos(π/sides), so along one + segment the least R that takes a point of it in is at an end, or where the segment + crosses the line out through a corner, on which a point's R is its distance. */ +function boreReach(sides, segs, x, y, turn = 0) { const step = 2 * Math.PI / sides, h = Math.cos(step / 2); const cx = [], cy = [], nx = [], ny = []; for (let k = 0; k < sides; k++) { - cx.push(Math.cos(k * step)); cy.push(Math.sin(k * step)); - nx.push(Math.cos((k + 0.5) * step)); ny.push(Math.sin((k + 0.5) * step)); + cx.push(Math.cos(k * step + turn)); cy.push(Math.sin(k * step + turn)); + nx.push(Math.cos((k + 0.5) * step + turn)); ny.push(Math.sin((k + 0.5) * step + turn)); } const at = (dx, dy) => { let m = -Infinity; @@ -4002,10 +4012,11 @@

Your drawers

magnet from beneath with a bowtie at 42 mm, which has 0.84 mm to spare and always built clean. */ const pad = platePad(cfg), again = !!known && known.pad === pad; - /* Each cut as { top, sites }: its ceiling, and each site in reach of it as how far a - bore's corner circle (`room`) and the bores themselves (`bore`, by their sides) can - grow before they meet the cut, and a magnet or screw (`body`) before it meets the - part in it. */ + /* Each cut as { top, segs, sites }: its ceiling, its footprint, and each site in reach + of it as how far a bore's corner circle (`room`) and the bores themselves (`bore`, by + their sides) can grow before they meet the cut, and a magnet or screw (`body`) before + it meets the part in it. Where the site stands from the cut's own origin (`x`, `y`) + is kept for a shank turned under its counterbore, measured for the sizes asked. */ const jointCuts = again ? known.list : []; /* Room past the most the cell or the floor leaves cannot stop a size, so nothing further off than that is measured, and nothing at all where neither leaves any. A @@ -4082,17 +4093,20 @@

Your drawers

const room = from(segs, x, y); if (room >= reach) continue; const bore = (n) => room > 0 ? boreReach(n, segs, x, y) : room; - sites.push({ room, body: from(part, x, y), bore: { 12: bore(12), 14: bore(14) } }); + sites.push({ x, y, room, body: from(part, x, y), bore: { 12: bore(12), 14: bore(14) } }); } - if (sites.length) jointCuts.push({ top: ceiling, sites }); + if (sites.length) jointCuts.push({ top: ceiling, segs, sites }); } } } /* The bores as fastenerCutter cuts them (MOUNT_BORE): how many sides, and the largest size whose corners stay inside a radius. Each stands its flats on its size, so its corners stand out past it, and a magnet's 0.1 mm over its radius where that is - further. */ + further. The shank is turned where the cutter turns it, and measured so: its turn + goes by the sizes asked, the head's and the magnet's as well as its own, so it is + the shank of this design that is measured. */ const BORE = { magnetD: MOUNT_BORE.magnet, screwHeadD: MOUNT_BORE.head, screwHoleD: MOUNT_BORE.hole }; + const shankTurn = cfg.screws ? MOUNT_BORE.screw(cfg).turn : 0; // the room the cell, the floor or a boss leaves, and the hole beside const own = { magnetD: fit(cfg.magnetSide === 'top' ? top : under), screwHoleD: fit(top), screwHeadD: fit(under) }; // a dovetail's notch takes a pocket from beneath that breaks into it; every other cut does not @@ -4106,6 +4120,9 @@

Your drawers

into, but not stop just short of. */ const jointFor = (f) => { const { sides, fits } = BORE[f]; + // the room a bore has, measured again for a shank turned under its counterbore + const turn = f === 'screwHoleD' ? shankTurn : 0; + const reach = (c, s) => !turn ? s.bore[sides] : s.room > 0 ? boreReach(sides, c.segs, s.x, s.y, turn) : s.room; const above = f === 'magnetD' && cfg.magnetSide === 'top'; // how high a pocket from beneath goes; a shank goes through const ceiling = f === 'magnetD' ? cfg.magnetH : f === 'screwHeadD' ? cfg.screwHeadDepth : Infinity; @@ -4121,10 +4138,10 @@

Your drawers

const level = Math.round(Math.abs(ceiling - c.top) * 1e6) / 1e6 < MOUNT_LEVEL; const stop = above ? 'floor' : !notch ? 'cut' : level ? 'level' : 'part'; for (const s of c.sites) { - const short = fits(s.bore[sides] - MOUNT_SEAM); + const bore = reach(c, s), short = fits(bore - MOUNT_SEAM); const size = stop === 'part' ? 2 * s.body : short; if (size < cap) { cap = size; why = stop; } - if (stop === 'part') near.push([short, fits(s.bore[sides])]); + if (stop === 'part') near.push([short, fits(bore)]); } } return { cap, why, near }; diff --git a/sw.js b/sw.js index 68e0186..b74fa1d 100644 --- a/sw.js +++ b/sw.js @@ -52,7 +52,7 @@ load from this site, and the app's icons and manifest, as paths from the root. VERSION is a hash of all of those files, names and bytes, and of this script as it is served, FILES filled in and everything else but VERSION itself. */ -const VERSION = "79f2004a1eb1"; +const VERSION = "bdcd9d9789bf"; const FILES = [ "./", "bins/", From 8d406b5a0d1ecdbe017c850fd084bfb8e65a474d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 19:12:47 +0000 Subject: [PATCH 17/58] Turn the shank where a magnet pocket's corner stands on its flat, as for a counterbore's MOUNT_BORE.hole.turn looked at the magnet pocket's corners only once the counterbore's already called for a turn. So a shank no counterbore corner came near was cut unturned even with a magnet's corner on a flat's plane, and the BSP split there: the review's 41.24 mm jigsaw link, a 3 mm shank under a 7.3 mm magnet from above whose corner stands 0.4 microns off a flat, built both pieces with 26 open edges and Download on, where main fails to build. The magnet's corners now call for the turn as the counterbore's do. The link, its 2.264 mm magnet and the 7.4 mm head beside it build whole, and #78's rows keep their test, with the 7.4 mm head added. Heads 7.3 to 7.7, depths 3 to 3.25, magnets 2.15 to 2.35 thick and pitches 41.2 to 41.34 close, but for 7.31 mm, #77's coincidence. The turn moves none of the 3,846 one-cell plates or 1,800 designs it was measured on, and of 1,500 random designs it now turns, 54 close and 1 opens, at that one size. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 22 ++++++++++++---------- src/core.js | 43 ++++++++++++++++++++++++++----------------- test/plate-audit.js | 13 +++++++++++-- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 2d4827b..3569308 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -708,16 +708,18 @@ currently installed): cuts' walls weave round the hole, and here and there a cell leaked by the hundred (#73: 2.03 mm over 2 with the corners on the sizes, 2.046 over 2 and 1.034 over 1 with the flats). Such a head is cut as none, as one no wider than the hole always was. And the - shank is turned π/84 about its axis where one of the counterbore's corners would - otherwise stand within 0.005 mm of a flat's plane carried on past the shank - (`MOUNT_BORE.hole.turn`): the BSP splits a magnet pocket's floor or roof along that - whole plane, the weld pulled the corner onto the split, and the plate came out open by - the dozen round every site (#74: 136 edges a piece for 1.0353 mm under 1.0873 with a - magnet from above at 42 mm, on main). The magnet pocket's corners stand on the same - rays, so where π/84 would bring one of those within 0.005 mm of a flat instead, the - shank takes the step of the 24 across those 30/7° that keeps both furthest off. Turned, - no corner of either comes within 0.0039 mm of a flat, and a shank no corner of the - counterbore's came near is cut as before. `MOUNT_BORE.screw` says whether a design's + shank is turned π/84 about its axis where one of the counterbore's corners, or one of + the magnet pocket's, would otherwise stand within 0.005 mm of a flat's plane carried on + past the shank (`MOUNT_BORE.hole.turn`): the BSP splits a magnet pocket's floor or roof + along that whole plane, the weld pulled the corner onto the split, and the plate came + out open by the dozen round every site (#74: 136 edges a piece for 1.0353 mm under + 1.0873 with a magnet from above at 42 mm, on main; and 26 a piece at 41.24 mm for a 3 mm + shank under a 7.3 mm magnet from above, whose corner stood 0.4 microns off a flat). The + two pockets' corners stand on the same rays, so where π/84 would bring one within 0.005 + mm of a flat, the shank takes the step of the 24 across those 30/7° that keeps both + furthest off. Turned, no corner of either comes within 0.0039 mm of a flat where the + magnet stands 0.3 mm or more outside the shank's corners, and a shank no corner of + either came near is cut as before. `MOUNT_BORE.screw` says whether a design's counterbore is cut and how far its shank is turned, for the cutter and for `mountLimits`, which measures that shank turned. - **The socket's corner clearance is not uniform.** Known, documented, deliberately not diff --git a/src/core.js b/src/core.js index 15a5932..f6d8902 100644 --- a/src/core.js +++ b/src/core.js @@ -926,21 +926,30 @@ function cylinder(cx, cy, r, z0, z1, seg) { * it was. A magnet pocket's corners stand on the same rays as the counterbore's, and π/84 * could bring one of those onto a flat instead: 4.85 mm under 7.246 with a 6 mm magnet * from above left 35 edges in the socket floor at 49.02 mm, and 5.773 under 12.053 25 at - * 55 mm, where the flats unturned miss them by 0.07 and 0.04 (unturned shanks meet them at - * other sizes: 4.9911 under 7.246 leaves 44 at 49.02 mm, with no turn). So where π/84 - * leaves a corner of either nearer than 0.005 mm, the turn is the one of 24 steps across - * those 30/7° that keeps them furthest off. Wherever the counterbore's corners call for a - * turn, no corner of either then comes within 0.0039 mm of a flat (6 and 8 mm magnets, - * heads up to 2.2 times the shank). Half a facet, π/12, clears the corners as well, but - * it squares two flats to the cell's edges, and at 37.67 mm those folded the socket floor - * beside the pocket. Measured on 3,846 one-cell plates with a magnet from above, 37.67 to - * 55 mm, shanks 1 to 5 mm and heads up to 1.15 times them, every 0.001 mm round these - * sizes: 546 open before, and 1 now, a size the turn leaves alone; none came out worse. - * On 1,800 random designs the turn moves, every kind of mount: 90 open before, 24 now. It - * closed 83 and opened 17: 14 at 42 mm with a magnet from beneath under a deeper - * counterbore, where one size in nine leaks unturned too, by the same slivers at the - * magnet pocket's corners in the underside, and 3 in corner bosses, each at that one size - * alone. */ + * 55 mm, where the flats unturned miss them by 0.07 and 0.04. So where π/84 leaves a + * corner of either nearer than 0.005 mm, the turn is the one of 24 steps across those + * 30/7° that keeps them furthest off. Wherever the counterbore's corners call for a turn, + * no corner of either then comes within 0.0039 mm of a flat (6 and 8 mm magnets, heads up + * to 2.2 times the shank). And the magnet's corners call for one as the counterbore's do, + * since a shank no counterbore corner comes near can have a magnet's corner on a flat + * all the same: at 41.24 mm a 3 mm shank under a 7.3 mm magnet from above, a corner 0.4 + * microns off a flat, left 26 edges a piece in the socket floor whatever the head, and + * 4.9911 under 7.246 with a 6 mm one left 44 at 49.02 mm. Wherever the magnet's + * corners stand 0.3 mm or more outside the shank's, none then comes within 0.0045 mm of + * a flat (magnets of 3 to 13 mm, heads up to 2.4 times the shank). Half a facet, π/12, + * clears the corners as well, but it squares two flats to the cell's edges, and at 37.67 + * mm those folded the socket floor beside the pocket. Measured on 3,846 one-cell plates + * with a magnet from above, 37.67 to 55 mm, shanks 1 to 5 mm and heads up to 1.15 times + * them, every 0.001 mm round these sizes: 546 open before, and 1 now, a size the turn + * leaves alone; none came out worse. On 1,800 random designs the turn moves, every kind + * of mount: 90 open before, 24 now. It closed 83 and opened 17: 14 at 42 mm with a magnet + * from beneath under a deeper counterbore, where one size in nine leaks unturned too, by + * the same slivers at the magnet pocket's corners in the underside, and 3 in corner + * bosses, each at that one size alone. The magnet's corners change the turn of none of + * those. On 1,500 random page designs that only a magnet's corner turns (34 to 56 mm; + * one cell, 2 x 2, and two pieces joined by dovetails or jigsaws; magnets of 3 to 12.7 mm + * either way up; a fifth on corner bosses): 66 bad before, 13 now. It closed 54 and + * opened 1, at that one size alone; of the 12 still bad, 10 are on corner bosses. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, @@ -953,7 +962,8 @@ const MOUNT_BORE = { // how far to turn the shank under a counterbore headD across, in a magnet pocket // magnetR out at its corners or none (see above) turn: (d, headD, magnetR) => { - const { head, hole } = MOUNT_BORE, rs = [head.r(headD)]; + // the magnet pocket's corners stand on the same rays as the counterbore's + const { head, hole } = MOUNT_BORE, rs = magnetR ? [head.r(headD), magnetR] : [head.r(headD)]; const clear = (t) => { // the nearest a corner comes to one of the flats' planes let g = Infinity; for (const R of rs) for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) @@ -961,7 +971,6 @@ const MOUNT_BORE = { return g; }; if (clear(0) >= 0.005) return 0; - if (magnetR) rs.push(magnetR); // the magnet pocket's corners stand on the same rays let best = Math.PI / 84, most = clear(best); if (most < 0.005) for (let j = 1; j < 24; j++) { const t = (Math.PI / 42) * j / 24, g = clear(t); diff --git a/test/plate-audit.js b/test/plate-audit.js index 7ce48f4..33f1d2d 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -1474,7 +1474,15 @@ function sectionArea(polys, z, dy) { * in the STL. A second run that comes out open throws now, so the plate fails to build, as * it did before, and the checks say so. These two rows are built as the page builds them * (its clearances and margins), and hold "threw in the seam repair, or watertight and - * oriented": whatever builds them later has to build them whole. */ + * oriented": whatever builds them later has to build them whole. + * + * With a bore's flats on its size, the same link cuts a counterbore 0.19 mm and a shank + * 0.1 mm wider, a cut the seam repair settles. It came out open instead, 26 edges a piece + * in the socket floor, and so did 38 of the 41 heads from 7.3 to 7.7 mm: a corner of the + * magnet's pocket stood 0.4 microns off the plane of a flat of the shank. The shank is + * turned for that now (MOUNT_BORE.hole.turn), so both rows build whole here, and so does + * the third, a 7.4 mm head, which main builds whole as well. Of those heads only 7.31 mm + * still leaks, its corners 0.001 mm inside the magnet's (#77). */ console.log('\na counterbore whose ceiling three cut lines cross at one point (#76):'); { const PLATE = { pitch: 39.07, drawerW: 170, drawerD: 90, bedW: 100, bedD: 400, marginMode: 'auto', @@ -1508,7 +1516,8 @@ console.log('\na counterbore whose ceiling three cut lines cross at one point (# connector: 'puzzle', clr: 0.2, screws: true, screwHoleD: 3, screwHeadDepth: 3.126, screwHeadD: 7.504, magnets: true, magnetSide: 'top', magnetD: 7.3, magnetH: 2.25 }; for (const [label, over] of [['41.24 mm jigsaw: 55 bad edges a piece', {}], - ['2.264 mm magnet: folded twice', { magnetH: 2.264 }]]) { + ['2.264 mm magnet: folded twice', { magnetH: 2.264 }], + ['7.4 mm head', { screwHeadD: 7.4 }]]) { const b = build({ ...JIGSAW, ...over }); const held = b.whole || /^healCsgSeams/.test(b.err || ''); console.log(` ${label.padEnd(38)} ${b.text}${held ? '' : ' NEITHER THREW NOR BUILT WHOLE'}`); From 10f8cf8fcaacded25c1664b2578c9631333185c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 19:12:47 +0000 Subject: [PATCH 18/58] Rebuild the pages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 43 ++++++++++++++++++++++++++----------------- index.html | 43 ++++++++++++++++++++++++++----------------- sw.js | 2 +- 3 files changed, 53 insertions(+), 35 deletions(-) diff --git a/bins/index.html b/bins/index.html index 7a9155e..32e304b 100644 --- a/bins/index.html +++ b/bins/index.html @@ -2532,21 +2532,30 @@

Your drawers

* it was. A magnet pocket's corners stand on the same rays as the counterbore's, and π/84 * could bring one of those onto a flat instead: 4.85 mm under 7.246 with a 6 mm magnet * from above left 35 edges in the socket floor at 49.02 mm, and 5.773 under 12.053 25 at - * 55 mm, where the flats unturned miss them by 0.07 and 0.04 (unturned shanks meet them at - * other sizes: 4.9911 under 7.246 leaves 44 at 49.02 mm, with no turn). So where π/84 - * leaves a corner of either nearer than 0.005 mm, the turn is the one of 24 steps across - * those 30/7° that keeps them furthest off. Wherever the counterbore's corners call for a - * turn, no corner of either then comes within 0.0039 mm of a flat (6 and 8 mm magnets, - * heads up to 2.2 times the shank). Half a facet, π/12, clears the corners as well, but - * it squares two flats to the cell's edges, and at 37.67 mm those folded the socket floor - * beside the pocket. Measured on 3,846 one-cell plates with a magnet from above, 37.67 to - * 55 mm, shanks 1 to 5 mm and heads up to 1.15 times them, every 0.001 mm round these - * sizes: 546 open before, and 1 now, a size the turn leaves alone; none came out worse. - * On 1,800 random designs the turn moves, every kind of mount: 90 open before, 24 now. It - * closed 83 and opened 17: 14 at 42 mm with a magnet from beneath under a deeper - * counterbore, where one size in nine leaks unturned too, by the same slivers at the - * magnet pocket's corners in the underside, and 3 in corner bosses, each at that one size - * alone. */ + * 55 mm, where the flats unturned miss them by 0.07 and 0.04. So where π/84 leaves a + * corner of either nearer than 0.005 mm, the turn is the one of 24 steps across those + * 30/7° that keeps them furthest off. Wherever the counterbore's corners call for a turn, + * no corner of either then comes within 0.0039 mm of a flat (6 and 8 mm magnets, heads up + * to 2.2 times the shank). And the magnet's corners call for one as the counterbore's do, + * since a shank no counterbore corner comes near can have a magnet's corner on a flat + * all the same: at 41.24 mm a 3 mm shank under a 7.3 mm magnet from above, a corner 0.4 + * microns off a flat, left 26 edges a piece in the socket floor whatever the head, and + * 4.9911 under 7.246 with a 6 mm one left 44 at 49.02 mm. Wherever the magnet's + * corners stand 0.3 mm or more outside the shank's, none then comes within 0.0045 mm of + * a flat (magnets of 3 to 13 mm, heads up to 2.4 times the shank). Half a facet, π/12, + * clears the corners as well, but it squares two flats to the cell's edges, and at 37.67 + * mm those folded the socket floor beside the pocket. Measured on 3,846 one-cell plates + * with a magnet from above, 37.67 to 55 mm, shanks 1 to 5 mm and heads up to 1.15 times + * them, every 0.001 mm round these sizes: 546 open before, and 1 now, a size the turn + * leaves alone; none came out worse. On 1,800 random designs the turn moves, every kind + * of mount: 90 open before, 24 now. It closed 83 and opened 17: 14 at 42 mm with a magnet + * from beneath under a deeper counterbore, where one size in nine leaks unturned too, by + * the same slivers at the magnet pocket's corners in the underside, and 3 in corner + * bosses, each at that one size alone. The magnet's corners change the turn of none of + * those. On 1,500 random page designs that only a magnet's corner turns (34 to 56 mm; + * one cell, 2 x 2, and two pieces joined by dovetails or jigsaws; magnets of 3 to 12.7 mm + * either way up; a fifth on corner bosses): 66 bad before, 13 now. It closed 54 and + * opened 1, at that one size alone; of the 12 still bad, 10 are on corner bosses. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, @@ -2559,7 +2568,8 @@

Your drawers

// how far to turn the shank under a counterbore headD across, in a magnet pocket // magnetR out at its corners or none (see above) turn: (d, headD, magnetR) => { - const { head, hole } = MOUNT_BORE, rs = [head.r(headD)]; + // the magnet pocket's corners stand on the same rays as the counterbore's + const { head, hole } = MOUNT_BORE, rs = magnetR ? [head.r(headD), magnetR] : [head.r(headD)]; const clear = (t) => { // the nearest a corner comes to one of the flats' planes let g = Infinity; for (const R of rs) for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) @@ -2567,7 +2577,6 @@

Your drawers

return g; }; if (clear(0) >= 0.005) return 0; - if (magnetR) rs.push(magnetR); // the magnet pocket's corners stand on the same rays let best = Math.PI / 84, most = clear(best); if (most < 0.005) for (let j = 1; j < 24; j++) { const t = (Math.PI / 42) * j / 24, g = clear(t); diff --git a/index.html b/index.html index c29a365..873668b 100644 --- a/index.html +++ b/index.html @@ -2319,21 +2319,30 @@

Your drawers

* it was. A magnet pocket's corners stand on the same rays as the counterbore's, and π/84 * could bring one of those onto a flat instead: 4.85 mm under 7.246 with a 6 mm magnet * from above left 35 edges in the socket floor at 49.02 mm, and 5.773 under 12.053 25 at - * 55 mm, where the flats unturned miss them by 0.07 and 0.04 (unturned shanks meet them at - * other sizes: 4.9911 under 7.246 leaves 44 at 49.02 mm, with no turn). So where π/84 - * leaves a corner of either nearer than 0.005 mm, the turn is the one of 24 steps across - * those 30/7° that keeps them furthest off. Wherever the counterbore's corners call for a - * turn, no corner of either then comes within 0.0039 mm of a flat (6 and 8 mm magnets, - * heads up to 2.2 times the shank). Half a facet, π/12, clears the corners as well, but - * it squares two flats to the cell's edges, and at 37.67 mm those folded the socket floor - * beside the pocket. Measured on 3,846 one-cell plates with a magnet from above, 37.67 to - * 55 mm, shanks 1 to 5 mm and heads up to 1.15 times them, every 0.001 mm round these - * sizes: 546 open before, and 1 now, a size the turn leaves alone; none came out worse. - * On 1,800 random designs the turn moves, every kind of mount: 90 open before, 24 now. It - * closed 83 and opened 17: 14 at 42 mm with a magnet from beneath under a deeper - * counterbore, where one size in nine leaks unturned too, by the same slivers at the - * magnet pocket's corners in the underside, and 3 in corner bosses, each at that one size - * alone. */ + * 55 mm, where the flats unturned miss them by 0.07 and 0.04. So where π/84 leaves a + * corner of either nearer than 0.005 mm, the turn is the one of 24 steps across those + * 30/7° that keeps them furthest off. Wherever the counterbore's corners call for a turn, + * no corner of either then comes within 0.0039 mm of a flat (6 and 8 mm magnets, heads up + * to 2.2 times the shank). And the magnet's corners call for one as the counterbore's do, + * since a shank no counterbore corner comes near can have a magnet's corner on a flat + * all the same: at 41.24 mm a 3 mm shank under a 7.3 mm magnet from above, a corner 0.4 + * microns off a flat, left 26 edges a piece in the socket floor whatever the head, and + * 4.9911 under 7.246 with a 6 mm one left 44 at 49.02 mm. Wherever the magnet's + * corners stand 0.3 mm or more outside the shank's, none then comes within 0.0045 mm of + * a flat (magnets of 3 to 13 mm, heads up to 2.4 times the shank). Half a facet, π/12, + * clears the corners as well, but it squares two flats to the cell's edges, and at 37.67 + * mm those folded the socket floor beside the pocket. Measured on 3,846 one-cell plates + * with a magnet from above, 37.67 to 55 mm, shanks 1 to 5 mm and heads up to 1.15 times + * them, every 0.001 mm round these sizes: 546 open before, and 1 now, a size the turn + * leaves alone; none came out worse. On 1,800 random designs the turn moves, every kind + * of mount: 90 open before, 24 now. It closed 83 and opened 17: 14 at 42 mm with a magnet + * from beneath under a deeper counterbore, where one size in nine leaks unturned too, by + * the same slivers at the magnet pocket's corners in the underside, and 3 in corner + * bosses, each at that one size alone. The magnet's corners change the turn of none of + * those. On 1,500 random page designs that only a magnet's corner turns (34 to 56 mm; + * one cell, 2 x 2, and two pieces joined by dovetails or jigsaws; magnets of 3 to 12.7 mm + * either way up; a fifth on corner bosses): 66 bad before, 13 now. It closed 54 and + * opened 1, at that one size alone; of the 12 still bad, 10 are on corner bosses. */ const MOUNT_BORE = { magnet: { sides: 14, r: (d) => Math.max(d/2 + 0.1, d/2 / Math.cos(Math.PI/14)), fits: (room) => 2 * Math.min(room - 0.1, room * Math.cos(Math.PI / 14)) }, @@ -2346,7 +2355,8 @@

Your drawers

// how far to turn the shank under a counterbore headD across, in a magnet pocket // magnetR out at its corners or none (see above) turn: (d, headD, magnetR) => { - const { head, hole } = MOUNT_BORE, rs = [head.r(headD)]; + // the magnet pocket's corners stand on the same rays as the counterbore's + const { head, hole } = MOUNT_BORE, rs = magnetR ? [head.r(headD), magnetR] : [head.r(headD)]; const clear = (t) => { // the nearest a corner comes to one of the flats' planes let g = Infinity; for (const R of rs) for (let k = 0; k < head.sides; k++) for (let m = 0; m < hole.sides; m++) @@ -2354,7 +2364,6 @@

Your drawers

return g; }; if (clear(0) >= 0.005) return 0; - if (magnetR) rs.push(magnetR); // the magnet pocket's corners stand on the same rays let best = Math.PI / 84, most = clear(best); if (most < 0.005) for (let j = 1; j < 24; j++) { const t = (Math.PI / 42) * j / 24, g = clear(t); diff --git a/sw.js b/sw.js index dd926af..d0d2e06 100644 --- a/sw.js +++ b/sw.js @@ -52,7 +52,7 @@ load from this site, and the app's icons and manifest, as paths from the root. VERSION is a hash of all of those files, names and bytes, and of this script as it is served, FILES filled in and everything else but VERSION itself. */ -const VERSION = "fcf7838578da"; +const VERSION = "418744ebfaa5"; const FILES = [ "./", "bins/", From fdb25b1725fe1abe9e213f6538198a7bffbf105b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 19:14:51 +0000 Subject: [PATCH 19/58] Keep the seam repair's guard tested with main's bores, now that the 41.24 mm link builds With its shank turned, the review's 41.24 mm jigsaw link no longer runs out of passes here, so #78's rows build whole and no longer reach the second run's guard. A fourth row cuts main's bores for it, 7.504 cos(pi/14) and 3 cos(pi/12), which still run out of passes and throw, here as on main. The comments that said the plate fails here now say where it still does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 5 +++-- src/core.js | 4 +++- src/ui.js | 6 +++--- test/plate-audit.js | 9 +++++++-- test/ui/plates-robustness.spec.js | 4 ++-- 5 files changed, 18 insertions(+), 10 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 3569308..31cb56d 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -905,5 +905,6 @@ currently installed): counterbore's ceiling 2 microns from an edge of its own. Every piece with a seam on its left failed to build, for heads from 7.3 mm to the cap; 2.339 and 2.341 mm built. The second run is kept only if it comes out closed: a jigsaw plate at 41.24 mm with magnets - from above settles open after the merge, and fails to build as it did before rather - than going out with holes. + from above and main's bores (a 7.504 mm counterbore and a 3 mm shank, corners on the + sizes) settles open after the merge, and fails to build as it did before rather than + going out with holes. diff --git a/src/core.js b/src/core.js index f6d8902..094fb80 100644 --- a/src/core.js +++ b/src/core.js @@ -402,7 +402,9 @@ function healCsgSeams(polys, again) { every soup, but not every one. None of 1,100 random plates of every joint and mount ran out; the turned cutters of a cell cut again do on a few engine-only cells (see the fastener cut in buildPiece), and so does a jigsaw plate at 41.24 mm with magnets - from above and a 3.126 mm counterbore, on the first cut of both its pieces. + from above and a 3.126 mm counterbore, on the first cut of both its pieces, with + main's 7.504 mm head over a 3 mm shank (the bores 7.504 cos(π/14) and 3 cos(π/12) here, + where a bore's flats stand on its size). The second run is kept only if it comes out closed, and that jigsaw plate is why. The merge lets its repair settle, but open, and returned that was 55 bad edges on diff --git a/src/ui.js b/src/ui.js index 1c720c0..626b53a 100644 --- a/src/ui.js +++ b/src/ui.js @@ -531,9 +531,9 @@ function warningsList() { under the cut map. Drawn as an error but not flagged `err`, which would stop the next build as well, and the next build, on any change, is what clears it. It says Download is off, not that nothing can be downloaded: the pieces built before it keep their own - STL buttons in the piece table. And it promises no size that gets past it: the 41.24 mm - jigsaw plate in plate-audit.js fails with a 7.504 mm head and builds with holes at - 7.502 mm, on main as well. */ + STL buttons in the piece table. And it promises no size that gets past it: a size a + hundredth off one that fails can build with holes, as the 41.24 mm jigsaw plate in + plate-audit.js did at 7.502 mm on main, beside a 7.504 mm head that failed. */ if (buildFailed) out.push({ failed: true, t: `Piece ${buildFailed} could not be built, so the build stopped ` + 'there and Download is off. That is a fault in this tool, not in the design; moving a cut ' + diff --git a/test/plate-audit.js b/test/plate-audit.js index 33f1d2d..0d129ba 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -1482,7 +1482,10 @@ function sectionArea(polys, z, dy) { * magnet's pocket stood 0.4 microns off the plane of a flat of the shank. The shank is * turned for that now (MOUNT_BORE.hole.turn), so both rows build whole here, and so does * the third, a 7.4 mm head, which main builds whole as well. Of those heads only 7.31 mm - * still leaks, its corners 0.001 mm inside the magnet's (#77). */ + * still leaks, its corners 0.001 mm inside the magnet's (#77). The fourth cuts main's + * bores, the head and shank whose polygons are main's 7.504 and 3 mm ones, 7.504 cos(π/14) + * and 3 cos(π/12): it runs out of passes here as on main and throws, so the second run's + * guard is still tested. */ console.log('\na counterbore whose ceiling three cut lines cross at one point (#76):'); { const PLATE = { pitch: 39.07, drawerW: 170, drawerD: 90, bedW: 100, bedD: 400, marginMode: 'auto', @@ -1517,7 +1520,9 @@ console.log('\na counterbore whose ceiling three cut lines cross at one point (# screwHeadD: 7.504, magnets: true, magnetSide: 'top', magnetD: 7.3, magnetH: 2.25 }; for (const [label, over] of [['41.24 mm jigsaw: 55 bad edges a piece', {}], ['2.264 mm magnet: folded twice', { magnetH: 2.264 }], - ['7.4 mm head', { screwHeadD: 7.4 }]]) { + ['7.4 mm head', { screwHeadD: 7.4 }], + ["main's bores", { screwHeadD: 7.504 * Math.cos(Math.PI / 14), + screwHoleD: 3 * Math.cos(Math.PI / 12) }]]) { const b = build({ ...JIGSAW, ...over }); const held = b.whole || /^healCsgSeams/.test(b.err || ''); console.log(` ${label.padEnd(38)} ${b.text}${held ? '' : ' NEITHER THREW NOR BUILT WHOLE'}`); diff --git a/test/ui/plates-robustness.spec.js b/test/ui/plates-robustness.spec.js index 7025f49..1f5455a 100644 --- a/test/ui/plates-robustness.spec.js +++ b/test/ui/plates-robustness.spec.js @@ -529,8 +529,8 @@ test.describe('a screw head that does not clear its shank is cut as none', () => said so already, but the list under the cut map, where a design is read for what is wrong with it, was empty, so Download was off with no reason given there. The failure is forced rather than taken from a design that fails, which would tie this test to an engine - bug a later fix should remove: #76's plate builds now (below), and the 41.24 mm jigsaw - plate in plate-audit.js still fails in the seam repair, as it does on main. */ + bug a later fix should remove: #76's plate builds now (below), and so does the 41.24 mm + jigsaw plate in plate-audit.js, which failed in the seam repair on main. */ test('a failed build says so in the checks, the table and the dialog, and Download goes off', async ({ page }) => { const pageErrors = []; From d617b5e334b8d371cc555a37352f577a95110b66 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 19:14:52 +0000 Subject: [PATCH 20/58] Rebuild the pages Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 4 +++- index.html | 10 ++++++---- sw.js | 2 +- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/bins/index.html b/bins/index.html index 32e304b..73ab6fb 100644 --- a/bins/index.html +++ b/bins/index.html @@ -2008,7 +2008,9 @@

Your drawers

every soup, but not every one. None of 1,100 random plates of every joint and mount ran out; the turned cutters of a cell cut again do on a few engine-only cells (see the fastener cut in buildPiece), and so does a jigsaw plate at 41.24 mm with magnets - from above and a 3.126 mm counterbore, on the first cut of both its pieces. + from above and a 3.126 mm counterbore, on the first cut of both its pieces, with + main's 7.504 mm head over a 3 mm shank (the bores 7.504 cos(π/14) and 3 cos(π/12) here, + where a bore's flats stand on its size). The second run is kept only if it comes out closed, and that jigsaw plate is why. The merge lets its repair settle, but open, and returned that was 55 bad edges on diff --git a/index.html b/index.html index 873668b..fdf28b8 100644 --- a/index.html +++ b/index.html @@ -1795,7 +1795,9 @@

Your drawers

every soup, but not every one. None of 1,100 random plates of every joint and mount ran out; the turned cutters of a cell cut again do on a few engine-only cells (see the fastener cut in buildPiece), and so does a jigsaw plate at 41.24 mm with magnets - from above and a 3.126 mm counterbore, on the first cut of both its pieces. + from above and a 3.126 mm counterbore, on the first cut of both its pieces, with + main's 7.504 mm head over a 3 mm shank (the bores 7.504 cos(π/14) and 3 cos(π/12) here, + where a bore's flats stand on its size). The second run is kept only if it comes out closed, and that jigsaw plate is why. The merge lets its repair settle, but open, and returned that was 55 bad edges on @@ -8228,9 +8230,9 @@

Your drawers

under the cut map. Drawn as an error but not flagged `err`, which would stop the next build as well, and the next build, on any change, is what clears it. It says Download is off, not that nothing can be downloaded: the pieces built before it keep their own - STL buttons in the piece table. And it promises no size that gets past it: the 41.24 mm - jigsaw plate in plate-audit.js fails with a 7.504 mm head and builds with holes at - 7.502 mm, on main as well. */ + STL buttons in the piece table. And it promises no size that gets past it: a size a + hundredth off one that fails can build with holes, as the 41.24 mm jigsaw plate in + plate-audit.js did at 7.502 mm on main, beside a 7.504 mm head that failed. */ if (buildFailed) out.push({ failed: true, t: `Piece ${buildFailed} could not be built, so the build stopped ` + 'there and Download is off. That is a fault in this tool, not in the design; moving a cut ' + diff --git a/sw.js b/sw.js index d0d2e06..e7ac401 100644 --- a/sw.js +++ b/sw.js @@ -52,7 +52,7 @@ load from this site, and the app's icons and manifest, as paths from the root. VERSION is a hash of all of those files, names and bytes, and of this script as it is served, FILES filled in and everything else but VERSION itself. */ -const VERSION = "418744ebfaa5"; +const VERSION = "624cd4380d11"; const FILES = [ "./", "bins/", From 1e09bc54edf0d6bea86988341342560775f86e3c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 20:30:34 +0000 Subject: [PATCH 21/58] Say at the screw head field when a head too narrow to clear its shank is cut as none A head wider than the hole that does not clear the shank's corners has been cut as no counterbore since #73, and only the README said so. A line under the screw fields now says it, and from what size a counterbore is cut, while neither size is refused. The README line reads the same helpers. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- src/template.html | 1 + src/ui.js | 23 ++++++++++++++++++----- test/ui/plates-robustness.spec.js | 14 ++++++++++---- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/template.html b/src/template.html index bbe51a2..0ba249d 100644 --- a/src/template.html +++ b/src/template.html @@ -283,6 +283,7 @@