From 9ac315806770d214a4d2399bb5029790544863f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 21:05:40 +0000 Subject: [PATCH 1/8] Plates with a screw bore nearly as wide as the magnet pocket it opens into no longer come out open on the bottom face A cell whose bottom face comes out open after its pockets' tries has that face fanned again from a point clear of the mounting cutters' corners alone, and its cuts taken again on it; the new cut is kept only if it is closed with nothing turned over and shares no more edges with the cells beside it. Cells that come out closed are fanned as before, so every design that already built closed keeps its bytes. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 22 ++++++++++++++++++++++ src/core.js | 43 ++++++++++++++++++++++++++++++++++++++++--- test/plate-audit.js | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+), 3 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 83dbb0b..721f2d6 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -906,6 +906,28 @@ currently installed): a 2.4 mm shank under a 4.8 mm head at 44.08 mm, loose, had a point within `FAN_CLEAR` of every centre `fanCentre` tried. The fan stayed put and the cell shipped six edges open. From the corners alone it moves 0.2 mm and the floor closes. + The bottom face is refanned the same way (#92). Its first fan keeps clear of every + point where the mounting cutters' walls stand on it, split points included, and where + a counterbore or a shank nearly as wide as the magnet pocket it opens into was unioned + with it, those are 140 points a cell against 56 corners: no point `fanCentre` tried + was clear of all of them, the fan stayed at the cell's middle with a spoke 5 microns + from a corner of a pocket, and 6 to 12 edges a cell were open at 42 mm. The first fan + from the corners alone closes them too, but it moves the fan of designs that build + closed as well (283 of 1,210 drawn round those sizes), and every one of those would + come out other bytes. So what a refan does is fan one face again from another point, + clear of the corners of the cutters' walls where they stand on it, and take every cut + again on it: the face is the same flat face, the solid is the same, and only the + triangles the cutters' planes cross differ. It is taken only on a cell still open after + its tries, with an open edge at that face's height, and only where the new fan point + is not the old; and the new cut is kept only if it comes out closed with nothing turned + over, sharing no more edges with the shells built beside it (the socket floor's keeps + one that is less open and no worse). A cell that comes out closed is built as before, + so a design that builds closed keeps its bytes. In the 4.16 × 2 family (shanks 2.1 to + 2.58 mm under a 4.34 mm head, from beneath at 42 mm) all 98 shanks that were open + close, and of 2,851 heads under a 6 × 2 magnet from beneath with a 3 mm hole 3 deep, + 570 of 571; the 3.909 mm head's 12 open edges are on the magnet pocket's roof, 2 mm up, + not on the bottom face, so it is not refanned. A design with a cell refanned takes + about 11% longer to build. - **A pocket that reaches a joint's cut goes bad where the two ceilings are level.** A key's recess, an H-clip's and a tab's notch are all cut up from under the plate, as a magnet pocket from beneath and a counterbore are. Run into one, the pocket comes out diff --git a/src/core.js b/src/core.js index a4c0af3..3ba0a06 100644 --- a/src/core.js +++ b/src/core.js @@ -2635,6 +2635,7 @@ function directCellRegion(clipped, prof, cx, cy, H, pad, arcSegs, half, avoid, f * side, which is a lottery rather than a property. A fan from an interior point has * no chords: every edge either lies on the outline or runs to one fixed point. */ const cc = fanCentre(oc, avoid); + polys.footAt = cc; // where the bottom cap was fanned from, so a refan can tell it moved for (let i = 0; i < n; i++) { const j = (i + 1) % n; const p = makePoly([[cc[0], cc[1], 0], [oc[j][0], oc[j][1], 0], [oc[i][0], oc[i][1], 0]]); @@ -3394,7 +3395,9 @@ function buildPiece(cfg, layout, piece, onStatus) { shipped with six open edges. A corner is where two walls that are not in line meet; a split point is where one wall was cut in two. From the corners alone the fan moves 0.2 mm and the floor closes. Only the socket floor's list, which only an open cell's - refan reads (buildPiece), so every cell that came out closed is built as before. */ + refan reads (buildPiece), so every cell that came out closed is built as before. + The bottom face has the same list for the same refan (footCorners, #92), and keeps + wallsAt's for its first fan, which every cell that comes out closed is built with. */ const cornersAt = (z) => { const ends = new Map(); // a wall end, to the directions of the walls that end there for (const p of cellFastener || []) { @@ -3416,7 +3419,7 @@ function buildPiece(cfg, layout, piece, onStatus) { return [...ends.values()].filter(({ dirs }) => dirs.some((a) => dirs.some((b) => Math.abs(a[0]*b[1] - a[1]*b[0]) > 1e-6))).map((e) => e.at); }; - const fastenerFoot = wallsAt(0), fastenerTop = pad > 0.01 ? cornersAt(pad) : []; + const fastenerFoot = wallsAt(0), footCorners = cornersAt(0), fastenerTop = pad > 0.01 ? cornersAt(pad) : []; // ---- connectors ---- const conn = pieceConnectors(cfg, layout, piece); @@ -4036,12 +4039,17 @@ function buildPiece(cfg, layout, piece, onStatus) { // the mounting cutters' walls, and with more room, of the joint's const avoid = halfX || halfY ? [] : [-1, 1].flatMap((sx) => [-1, 1].flatMap((sy) => fastenerFoot.map(([u, v]) => [cx + sx*cfg.holeOffset + u, cy + sy*cfg.holeOffset + v]))); + // the same with the mounting cutters' corners alone, for a cell that comes out open on + // the bottom face (below) + const footAvoid = halfX || halfY ? [] : [-1, 1].flatMap((sx) => [-1, 1].flatMap((sy) => + footCorners.map(([u, v]) => [cx + sx*cfg.holeOffset + u, cy + sy*cfg.holeOffset + v]))); for (const p of [...cuts.notch, ...cuts.key, ...cuts.puzzle]) p.verts.forEach((a, i) => { const b = p.verts[(i + 1) % p.verts.length]; if ((a[2] < 0) === (b[2] < 0)) return; const t = a[2] / (a[2] - b[2]); - avoid.push([a[0] + t*(b[0] - a[0]), a[1] + t*(b[1] - a[1]), FAN_JOINT]); + const at = [a[0] + t*(b[0] - a[0]), a[1] + t*(b[1] - a[1]), FAN_JOINT]; + avoid.push(at); footAvoid.push(at); }); // and the socket floor's, of the mounting cutters' corners there const floorAvoid = halfX || halfY ? [] : [-1, 1].flatMap((sx) => [-1, 1].flatMap((sy) => @@ -4179,6 +4187,35 @@ function buildPiece(cfg, layout, piece, onStatus) { if (again && again.open < region.open && !worse(again, region)) region = again; else base = first; } + /* Still open on the bottom face, with pockets standing on it (#92): the bottom cap + fanned again clear of the mounting cutters' corners alone, and every cut taken + again on it. The first fan keeps clear of every point of their walls where they + stand on the face (wallsAt), and where two bores were unioned that is their split + points as well as their corners. A counterbore or a shank nearly as wide as the + magnet pocket it opens into is split all round: a 6 x 2 magnet from beneath over a + 3 mm hole with a 4 mm head 3 deep, at 42 mm, has 140 such points in a cell where its + corners are 56. Every point fanCentre tries had a spoke within FAN_CLEAR of one of + them, so the fan stayed at the cell's middle, with a spoke 5 microns from a corner + of a magnet pocket, and the sliver of the face between them went: 9 open edges, 6 to + 12 across the sizes that did it, with Download on. From the corners alone the fan + moves 0.1 mm and the cell closes, as the socket floor's does above (cornersAt). + Kept only if it comes out closed with nothing turned over, sharing no more edges + with the shells built beside it than the first cut; otherwise the first cut + stands. Only a cell open at the bottom face's height whose fan moves is cut again, + so a cell that comes out closed is fanned as before, and every design that builds + closed builds the same bytes. */ + if (region.open && cuts.fastener.length && openAt(region, 0)) { + const first = base; + base = directCellRegion(clipped, prof, cx, cy, H, pad, cfg.arcSegs || 6, + undefined, footAvoid, first.floorMoved ? floorAvoid : undefined); + let again = null; + if (base.footAt[0] !== first.footAt[0] || base.footAt[1] !== first.footAt[1]) + try { again = cutCell(null); } catch (e) { /* the first cut stands */ } + const near = again && !again.open && !again.turned ? builtBeside(own) : []; + if (again && !again.open && !again.turned && sharedAll(again, own, near) <= sharedAll(region, own, near)) + region = again; + else base = first; + } if (cuts.notch.length || cuts.key.length || cuts.puzzle.length) { const beside = jointCells.filter((c) => c.box[0] < x1 && x0 < c.box[2] && c.box[1] < y1 && y0 < c.box[3]); diff --git a/test/plate-audit.js b/test/plate-audit.js index 4f53351..e55c88b 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -2281,6 +2281,42 @@ console.log('\nthe other limits, built at their ends:'); console.log(` ${nm.padEnd(38)} ${taken ? '' : 'REFUSED, '}${leakText(r)}${folds ? `, ${folds} FOLDS` : ''}`); if (!taken || r.bad || folds) bad++; } + /* A bore nearly as wide as the magnet pocket it opens into, where the two stand on the + bottom face (#92). That face is a fan held clear of every point where the mounting + cutters' walls stand on it (fanCentre), and where a counterbore or a shank was unioned + with a pocket it nearly fills, the BSP split each wall along the other's planes all + round: 140 points in a cell where the corners are 56. No point fanCentre tries was + clear of all of them, so the fan stayed at the cell's middle, a spoke a few microns + from a corner of a pocket, and the sliver of the face between them went: 6 to 12 open + edges a cell at 42 mm with Download on, where the magnet alone builds closed. A 6 x 2 + magnet from beneath over a 3 mm hole 3 deep at three heads; the 4.16 x 2 magnet over a + 2.58 mm shank under a 4.34 mm head; a bare 2.98 mm shank under a 3 x 2 magnet from + above at 46 mm; and four of the round sizes the issue typed. A cell open at the bottom + face's height has that face fanned again clear of the corners alone now (buildPiece). + Each is a size the page takes, and has to build closed, with no folds. */ + for (const [nm, o] of [ + ['6 x 2 below, 3 under 4, 3 deep', { screwHeadD: 4, screwHeadDepth: 3 }], + ['6 x 2 below, 3 under 5.53, 3 deep', { screwHeadD: 5.53, screwHeadDepth: 3 }], + ['6 x 2 below, 3 under 5.93, 3 deep', { screwHeadD: 5.93, screwHeadDepth: 3 }], + ['4.16 x 2 below, 2.58 under 4.34', { magnetD: 4.16, screwHoleD: 2.58, screwHeadD: 4.34 }], + ['3 x 2 above, 2.98 bare, 46', { pitch: 46, drawerW: 46, drawerD: 46, magnetD: 3, magnetSide: 'top', + screwHoleD: 2.98, screwHeadD: 2.98 }], + ['4 x 1.5 above, 2.5 under 6.05, 1.5 deep', { magnetD: 4, magnetH: 1.5, magnetSide: 'top', screwHoleD: 2.5, + screwHeadD: 6.05, screwHeadDepth: 1.5 }], + ['6 x 1.5 below, 3.5 under 4.75', { magnetH: 1.5, screwHoleD: 3.5, screwHeadD: 4.75 }], + ['3 x 3 above, 2.5 under 4.25, 1 deep', { magnetD: 3, magnetH: 3, magnetSide: 'top', screwHoleD: 2.5, + screwHeadD: 4.25, screwHeadDepth: 1 }], + ['6 x 3 below, 2 under 4.9, 4 deep', { magnetH: 3, screwHoleD: 2, screwHeadD: 4.9, screwHeadDepth: 4 }], + ]) { + const at = { pitch: 42, drawerW: 42, drawerD: 42, connector: 'none', magnets: true, screws: true, ...o }; + const cfg = designCfg(at), lim = G.mountLimits(cfg, G.computeLayout(cfg)); + const taken = ['magnetD', 'screwHoleD', 'screwHeadD'].every((f) => cfg[f] <= lim[f] + 1e-9 && + !lim.gaps[f].some(([a, b]) => cfg[f] >= a - 1e-9 && cfg[f] <= b + 1e-9)); + const r = buildAll(at); + const folds = r.pieces.reduce((s, pp) => s + checkOrientation(pp).folds, 0); + console.log(` ${nm.padEnd(38)} ${taken ? '' : 'REFUSED, '}${leakText(r)}${folds ? `, ${folds} FOLDS` : ''}`); + if (!taken || r.bad || folds) bad++; + } /* A counterbore's corner on the plane of one of the magnet pocket's flats. The two are 14-gons on the same rays, so a corner of one stands an odd multiple of π/14 off a normal of the other's flats, and at some sizes that puts it on the flat's plane From 0385888517dd1a4c79163fcfb8b597c27c9b46eb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 21:17:05 +0000 Subject: [PATCH 2/8] Corner pockets with screws or a magnet from above no longer come out open on a boss's top A boss whose pocket still comes out open after every try has its top fanned again from a point inside it clear of the corners of the walls that open on the top, and its pocket cut again; the new cut is kept only if it is closed with nothing turned over and shares no more edges with the shells beside it. Bosses that come out closed keep the fan from their corner, so every design that already built closed keeps its bytes. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 16 ++++++++++++++ src/core.js | 51 ++++++++++++++++++++++++++++++++++++++++++--- test/plate-audit.js | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 721f2d6..fa8dbc9 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -928,6 +928,22 @@ currently installed): 570 of 571; the 3.909 mm head's 12 open edges are on the magnet pocket's roof, 2 mm up, not on the bottom face, so it is not refanned. A design with a cell refanned takes about 11% longer to build. + A corner boss's top is a cap as well, and is refanned the same way (#91). `extrudePoly` + fans a convex cap from one corner of its outline (the ear clip takes the ear at its + first point every time), which for half the bosses is the cell's corner. A spoke from + there passed 1.3 microns from a corner of a 2 mm shank opening on the top at 34.32 mm, + and 5.3 from a corner of a magnet's pocket from above at 34.73 and 38.95 mm, and the + sliver went: 3 open edges a cell that none of `cutAgain`'s tries closed. The cap is + convex, so any point inside it will do: a boss still open after those tries has its top + fanned again from the point `fanCentre` picks clear of the corners of its cutter's + walls at the top (`refanTop`), the pocket cut again, and the result kept on the same + terms as the bottom face's. A boss that comes out closed keeps the fan from its corner. + Only the bosses no joint reaches are refanned: over 3,824 designs with each joint that + reaches the bosses, two pieces from 34 to 46.65 mm every 0.05, with screws, a magnet + from beneath and screws, or a magnet from above, each at its cap, none of the 6,884 + bosses a joint's cut reaches came out open, so such a boss is built as it was. Of the + 47,416 others, 15 were open, five designs at 43.9 mm with a 4.9 mm magnet from above + (its cap there), and each is closed by its refan. - **A pocket that reaches a joint's cut goes bad where the two ceilings are level.** A key's recess, an H-clip's and a tab's notch are all cut up from under the plate, as a magnet pocket from beneath and a counterbore are. Run into one, the pocket comes out diff --git a/src/core.js b/src/core.js index 3ba0a06..cfc9329 100644 --- a/src/core.js +++ b/src/core.js @@ -3397,10 +3397,11 @@ function buildPiece(cfg, layout, piece, onStatus) { 0.2 mm and the floor closes. Only the socket floor's list, which only an open cell's refan reads (buildPiece), so every cell that came out closed is built as before. The bottom face has the same list for the same refan (footCorners, #92), and keeps - wallsAt's for its first fan, which every cell that comes out closed is built with. */ - const cornersAt = (z) => { + wallsAt's for its first fan, which every cell that comes out closed is built with. + `cutter` is another cutter's, a corner boss's (#91). */ + const cornersAt = (z, cutter = cellFastener) => { const ends = new Map(); // a wall end, to the directions of the walls that end there - for (const p of cellFastener || []) { + for (const p of cutter || []) { const zs = p.verts.map((v) => v[2]); if (!(Math.min(...zs) < z && Math.max(...zs) > z) || Math.abs(p.plane.n[2]) > 1e-9) continue; const d = [-p.plane.n[1], p.plane.n[0]]; // along the wall @@ -4304,6 +4305,45 @@ function buildPiece(cfg, layout, piece, onStatus) { // through the top: mountLimits refuses one rather than this growing past it const bossH = bossHeight(cfg); const bossFastener = fastenerCutter(cfg, bossH - cfg.magnetH, bossH + 0.5, bossH + 0.5); + /* A boss's top, fanned again where its pocket comes out open after every try (#91). + extrudePoly fans a convex cap from one corner of its outline (the ear clip takes + the ear at its first point every time), and for half the bosses that is the cell's + corner, with a spoke to each point of the rounded inner corner. A screw's shank or a + pocket from above opens on the top, and where a spoke passes a few microns from a + corner of its walls, the two walls meeting there cross the spoke a couple of + thousandths apart, the weld takes the crossings for one point, and the sliver of + top between them and the corner goes: 1.3 microns from a 2 mm shank's corner at + 34.32 mm, 5.3 from a corner of a 4 or 6 mm magnet's pocket from above at 34.73 and + 38.95 mm. cutAgain's tries closed one boss of the two and left the other with 3 open + edges, a cell, with Download on. The cap is convex, so it can be fanned from any + point inside it: it is fanned again from the one fanCentre picks clear of the + corners of the cutter's walls where they stand at the boss's top (cornersAt), and + the pocket cut again on it, and that is kept if it comes out closed with nothing + turned over, sharing no more edges with the shells built beside it than the first + cut. A boss that comes out closed is built as before, fanned from its corner, and + so is every boss of a design that builds closed. `cut` cuts the new prism as the + first was cut. */ + const refanTop = (solid, outline, z, cutter, sites, cut) => { + const corners = cornersAt(z, cutter); + const c = fanCentre(outline, sites.flatMap((bs) => + corners.map(([u, v]) => [bs.ccx + bs.sx*off + u, bs.ccy + bs.sy*off + v]))); + const ccw = polyArea2D(outline) < 0 ? outline.slice().reverse() : outline; + const prism = extrudePoly(outline, 0, z).filter((p) => p.plane.n[2] < 0.5); // all but the top + ccw.forEach((p, i) => { + const q = ccw[(i + 1) % ccw.length], tri = makePoly([[c[0], c[1], z], [p[0], p[1], z], [q[0], q[1], z]]); + if (tri) prism.push(tri); + }); + let again; + try { again = cut(prism); } catch (e) { return solid; } // the first cut stands + if (!again || again.open || again.turned) return solid; + const box = flatBox(solid), near = []; + for (const s of shells) { + const b = flatBox(s); + if (b[0] <= box[2] + 1e-3 && box[0] <= b[2] + 1e-3 && b[1] <= box[3] + 1e-3 && box[1] <= b[3] + 1e-3) + near.push({ box: b, polys: s }); + } + return sharedAll(again, box, near) <= sharedAll(solid, box, near) ? again : solid; + }; /* The joint's cuts from beneath are taken out of the bosses as well, after their pockets: the same solids the cells are cut with. A wall key's recess, an H-clip's and a dovetail's notch sit where four cells meet, which is where four bosses meet, @@ -4397,6 +4437,11 @@ function buildPiece(cfg, layout, piece, onStatus) { const pocket = movePolys(bossFastener, ccx + sx*off, ccy + sy*off); const first = csgSubtract(boss, pocket); boss = first.open ? cutAgain(boss, pocket, first) : first; + if (boss.open) // its top fanned again (refanTop) + boss = refanTop(boss, world, bossH, bossFastener, [all[n]], (prism) => { + const again = csgSubtract(prism, pocket); + return again.open ? cutAgain(prism, pocket, again) : again; + }); } shells.push(boss); continue; diff --git a/test/plate-audit.js b/test/plate-audit.js index e55c88b..d072378 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -2706,6 +2706,43 @@ console.log('\ncorner bosses beside a joint cut from beneath:'); console.log(` ${'wall puzzle keys at 36.13 mm, screws'.padEnd(36)} ${leakText(r)}, ${plain.bad} with no joint${good ? '' : ' FAIL'}`); if (!good) bad++; } + /* A boss's top is fanned from a corner of its outline, the cell's corner for half of + them, and where a spoke passes a few microns from a corner of the walls that open on + the top, the sliver between goes (#91): a 2 mm shank at 34.32 mm, 1.3 microns, alone + or under a 6 mm magnet from beneath, a 4 mm magnet's pocket from above at 34.73 mm + and a 6 mm one's at 38.95, 5.3 microns, each with the head at the field's cap; and + with no screw, a 4.9 mm magnet from above at 43.9 mm, its cap there. Every try of + cutAgain left one boss a cell open, 3 edges, and 6 to 15 over 2 x 2, with Download + on, until a boss still open after them had its top fanned again clear of those + corners (refanTop in buildPiece). Each has to be taken and build with nothing open + and no folds; the 2 x 2 pieces keep the edges their bosses share (quarantined + above). */ + for (const [name, n, over] of [ + ['34.32 mm, 2 mm hole', 1, { pitch: 34.32, screwHoleD: 2, screwHeadD: 6.1 }], + ['34.32 mm, 2 mm hole, 6 mm magnet below', 1, { pitch: 34.32, magnets: true, screwHoleD: 2, screwHeadD: 6.1 }], + ['34.73 mm, 2 mm hole, 4 mm magnet above', 1, { pitch: 34.73, magnets: true, magnetD: 4, magnetSide: 'top', + screwHoleD: 2, screwHeadD: 6.5 }], + ['38.95 mm, 3 mm hole, 6 mm magnet above', 1, { pitch: 38.95, magnets: true, magnetSide: 'top', screwHoleD: 3, + screwHeadD: 9.7 }], + ['43.9 mm, 4.9 mm magnet above, no screw', 1, { pitch: 43.9, magnets: true, magnetD: 4.9, magnetSide: 'top', + screws: false }], + ['34.32 mm, 2 mm hole, 2 x 2', 2, { pitch: 34.32, screwHoleD: 2, screwHeadD: 6.1 }], + ['34.73 mm, 4 mm magnet above, 2 x 2', 2, { pitch: 34.73, magnets: true, magnetD: 4, magnetSide: 'top', + screwHoleD: 2, screwHeadD: 6.5 }], + ['38.95 mm, 6 mm magnet above, 2 x 2', 2, { pitch: 38.95, magnets: true, magnetSide: 'top', screwHoleD: 3, + screwHeadD: 9.7 }]]) { + const at = { drawerW: n * over.pitch, drawerD: n * over.pitch, bedW: 400, bedD: 400, baseMode: 'bosses', + connector: 'none', screws: true, ...over }; + const cfg = designCfg(at), lim = G.mountLimits(cfg, G.computeLayout(cfg)); + const taken = (at.magnets ? ['magnetD'] : []).concat(at.screws ? ['screwHoleD', 'screwHeadD'] : []).every((f) => + cfg[f] <= lim[f] + 1e-9 && !lim.gaps[f].some(([a, b]) => cfg[f] >= a - 1e-9 && cfg[f] <= b + 1e-9)); + const r = buildAll(at); + const folds = r.pieces.reduce((s, pp) => s + checkOrientation(pp).folds, 0); + const good = taken && !r.open && !folds; + console.log(` ${name.padEnd(40)} ${taken ? '' : 'REFUSED, '}${leakText(r)}${folds ? `, ${folds} FOLDS` : ''}` + + `${good ? '' : ' FAIL'}`); + if (!good) bad++; + } /* A boss is its pocket's depth and 0.8 (a magnet) or 1.0 (a screw head) tall, and one whose top stood level with the ceiling of the cut that reaches it came out open there: a dovetail's notch is 2.4 mm tall, an H-clip's recess 2.3 and a wall key's 2.0. These From 24a404e3e8cc23e0201e8ac9eedaa548807e17cb Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 21:17:48 +0000 Subject: [PATCH 3/8] Rebuild the pages Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 92 ++++++++++++++++++++++++++++++++++++++++++++++--- index.html | 92 ++++++++++++++++++++++++++++++++++++++++++++++--- sw.js | 2 +- 3 files changed, 175 insertions(+), 11 deletions(-) diff --git a/bins/index.html b/bins/index.html index 8643b9b..178c490 100644 --- a/bins/index.html +++ b/bins/index.html @@ -4241,6 +4241,7 @@

Your drawers

* side, which is a lottery rather than a property. A fan from an interior point has * no chords: every edge either lies on the outline or runs to one fixed point. */ const cc = fanCentre(oc, avoid); + polys.footAt = cc; // where the bottom cap was fanned from, so a refan can tell it moved for (let i = 0; i < n; i++) { const j = (i + 1) % n; const p = makePoly([[cc[0], cc[1], 0], [oc[j][0], oc[j][1], 0], [oc[i][0], oc[i][1], 0]]); @@ -5000,10 +5001,13 @@

Your drawers

shipped with six open edges. A corner is where two walls that are not in line meet; a split point is where one wall was cut in two. From the corners alone the fan moves 0.2 mm and the floor closes. Only the socket floor's list, which only an open cell's - refan reads (buildPiece), so every cell that came out closed is built as before. */ - const cornersAt = (z) => { + refan reads (buildPiece), so every cell that came out closed is built as before. + The bottom face has the same list for the same refan (footCorners, #92), and keeps + wallsAt's for its first fan, which every cell that comes out closed is built with. + `cutter` is another cutter's, a corner boss's (#91). */ + const cornersAt = (z, cutter = cellFastener) => { const ends = new Map(); // a wall end, to the directions of the walls that end there - for (const p of cellFastener || []) { + for (const p of cutter || []) { const zs = p.verts.map((v) => v[2]); if (!(Math.min(...zs) < z && Math.max(...zs) > z) || Math.abs(p.plane.n[2]) > 1e-9) continue; const d = [-p.plane.n[1], p.plane.n[0]]; // along the wall @@ -5022,7 +5026,7 @@

Your drawers

return [...ends.values()].filter(({ dirs }) => dirs.some((a) => dirs.some((b) => Math.abs(a[0]*b[1] - a[1]*b[0]) > 1e-6))).map((e) => e.at); }; - const fastenerFoot = wallsAt(0), fastenerTop = pad > 0.01 ? cornersAt(pad) : []; + const fastenerFoot = wallsAt(0), footCorners = cornersAt(0), fastenerTop = pad > 0.01 ? cornersAt(pad) : []; // ---- connectors ---- const conn = pieceConnectors(cfg, layout, piece); @@ -5642,12 +5646,17 @@

Your drawers

// the mounting cutters' walls, and with more room, of the joint's const avoid = halfX || halfY ? [] : [-1, 1].flatMap((sx) => [-1, 1].flatMap((sy) => fastenerFoot.map(([u, v]) => [cx + sx*cfg.holeOffset + u, cy + sy*cfg.holeOffset + v]))); + // the same with the mounting cutters' corners alone, for a cell that comes out open on + // the bottom face (below) + const footAvoid = halfX || halfY ? [] : [-1, 1].flatMap((sx) => [-1, 1].flatMap((sy) => + footCorners.map(([u, v]) => [cx + sx*cfg.holeOffset + u, cy + sy*cfg.holeOffset + v]))); for (const p of [...cuts.notch, ...cuts.key, ...cuts.puzzle]) p.verts.forEach((a, i) => { const b = p.verts[(i + 1) % p.verts.length]; if ((a[2] < 0) === (b[2] < 0)) return; const t = a[2] / (a[2] - b[2]); - avoid.push([a[0] + t*(b[0] - a[0]), a[1] + t*(b[1] - a[1]), FAN_JOINT]); + const at = [a[0] + t*(b[0] - a[0]), a[1] + t*(b[1] - a[1]), FAN_JOINT]; + avoid.push(at); footAvoid.push(at); }); // and the socket floor's, of the mounting cutters' corners there const floorAvoid = halfX || halfY ? [] : [-1, 1].flatMap((sx) => [-1, 1].flatMap((sy) => @@ -5785,6 +5794,35 @@

Your drawers

if (again && again.open < region.open && !worse(again, region)) region = again; else base = first; } + /* Still open on the bottom face, with pockets standing on it (#92): the bottom cap + fanned again clear of the mounting cutters' corners alone, and every cut taken + again on it. The first fan keeps clear of every point of their walls where they + stand on the face (wallsAt), and where two bores were unioned that is their split + points as well as their corners. A counterbore or a shank nearly as wide as the + magnet pocket it opens into is split all round: a 6 x 2 magnet from beneath over a + 3 mm hole with a 4 mm head 3 deep, at 42 mm, has 140 such points in a cell where its + corners are 56. Every point fanCentre tries had a spoke within FAN_CLEAR of one of + them, so the fan stayed at the cell's middle, with a spoke 5 microns from a corner + of a magnet pocket, and the sliver of the face between them went: 9 open edges, 6 to + 12 across the sizes that did it, with Download on. From the corners alone the fan + moves 0.1 mm and the cell closes, as the socket floor's does above (cornersAt). + Kept only if it comes out closed with nothing turned over, sharing no more edges + with the shells built beside it than the first cut; otherwise the first cut + stands. Only a cell open at the bottom face's height whose fan moves is cut again, + so a cell that comes out closed is fanned as before, and every design that builds + closed builds the same bytes. */ + if (region.open && cuts.fastener.length && openAt(region, 0)) { + const first = base; + base = directCellRegion(clipped, prof, cx, cy, H, pad, cfg.arcSegs || 6, + undefined, footAvoid, first.floorMoved ? floorAvoid : undefined); + let again = null; + if (base.footAt[0] !== first.footAt[0] || base.footAt[1] !== first.footAt[1]) + try { again = cutCell(null); } catch (e) { /* the first cut stands */ } + const near = again && !again.open && !again.turned ? builtBeside(own) : []; + if (again && !again.open && !again.turned && sharedAll(again, own, near) <= sharedAll(region, own, near)) + region = again; + else base = first; + } if (cuts.notch.length || cuts.key.length || cuts.puzzle.length) { const beside = jointCells.filter((c) => c.box[0] < x1 && x0 < c.box[2] && c.box[1] < y1 && y0 < c.box[3]); @@ -5873,6 +5911,45 @@

Your drawers

// through the top: mountLimits refuses one rather than this growing past it const bossH = bossHeight(cfg); const bossFastener = fastenerCutter(cfg, bossH - cfg.magnetH, bossH + 0.5, bossH + 0.5); + /* A boss's top, fanned again where its pocket comes out open after every try (#91). + extrudePoly fans a convex cap from one corner of its outline (the ear clip takes + the ear at its first point every time), and for half the bosses that is the cell's + corner, with a spoke to each point of the rounded inner corner. A screw's shank or a + pocket from above opens on the top, and where a spoke passes a few microns from a + corner of its walls, the two walls meeting there cross the spoke a couple of + thousandths apart, the weld takes the crossings for one point, and the sliver of + top between them and the corner goes: 1.3 microns from a 2 mm shank's corner at + 34.32 mm, 5.3 from a corner of a 4 or 6 mm magnet's pocket from above at 34.73 and + 38.95 mm. cutAgain's tries closed one boss of the two and left the other with 3 open + edges, a cell, with Download on. The cap is convex, so it can be fanned from any + point inside it: it is fanned again from the one fanCentre picks clear of the + corners of the cutter's walls where they stand at the boss's top (cornersAt), and + the pocket cut again on it, and that is kept if it comes out closed with nothing + turned over, sharing no more edges with the shells built beside it than the first + cut. A boss that comes out closed is built as before, fanned from its corner, and + so is every boss of a design that builds closed. `cut` cuts the new prism as the + first was cut. */ + const refanTop = (solid, outline, z, cutter, sites, cut) => { + const corners = cornersAt(z, cutter); + const c = fanCentre(outline, sites.flatMap((bs) => + corners.map(([u, v]) => [bs.ccx + bs.sx*off + u, bs.ccy + bs.sy*off + v]))); + const ccw = polyArea2D(outline) < 0 ? outline.slice().reverse() : outline; + const prism = extrudePoly(outline, 0, z).filter((p) => p.plane.n[2] < 0.5); // all but the top + ccw.forEach((p, i) => { + const q = ccw[(i + 1) % ccw.length], tri = makePoly([[c[0], c[1], z], [p[0], p[1], z], [q[0], q[1], z]]); + if (tri) prism.push(tri); + }); + let again; + try { again = cut(prism); } catch (e) { return solid; } // the first cut stands + if (!again || again.open || again.turned) return solid; + const box = flatBox(solid), near = []; + for (const s of shells) { + const b = flatBox(s); + if (b[0] <= box[2] + 1e-3 && box[0] <= b[2] + 1e-3 && b[1] <= box[3] + 1e-3 && box[1] <= b[3] + 1e-3) + near.push({ box: b, polys: s }); + } + return sharedAll(again, box, near) <= sharedAll(solid, box, near) ? again : solid; + }; /* The joint's cuts from beneath are taken out of the bosses as well, after their pockets: the same solids the cells are cut with. A wall key's recess, an H-clip's and a dovetail's notch sit where four cells meet, which is where four bosses meet, @@ -5966,6 +6043,11 @@

Your drawers

const pocket = movePolys(bossFastener, ccx + sx*off, ccy + sy*off); const first = csgSubtract(boss, pocket); boss = first.open ? cutAgain(boss, pocket, first) : first; + if (boss.open) // its top fanned again (refanTop) + boss = refanTop(boss, world, bossH, bossFastener, [all[n]], (prism) => { + const again = csgSubtract(prism, pocket); + return again.open ? cutAgain(prism, pocket, again) : again; + }); } shells.push(boss); continue; diff --git a/index.html b/index.html index 4791f5a..1e4d0a5 100644 --- a/index.html +++ b/index.html @@ -4029,6 +4029,7 @@

Your drawers

* side, which is a lottery rather than a property. A fan from an interior point has * no chords: every edge either lies on the outline or runs to one fixed point. */ const cc = fanCentre(oc, avoid); + polys.footAt = cc; // where the bottom cap was fanned from, so a refan can tell it moved for (let i = 0; i < n; i++) { const j = (i + 1) % n; const p = makePoly([[cc[0], cc[1], 0], [oc[j][0], oc[j][1], 0], [oc[i][0], oc[i][1], 0]]); @@ -4788,10 +4789,13 @@

Your drawers

shipped with six open edges. A corner is where two walls that are not in line meet; a split point is where one wall was cut in two. From the corners alone the fan moves 0.2 mm and the floor closes. Only the socket floor's list, which only an open cell's - refan reads (buildPiece), so every cell that came out closed is built as before. */ - const cornersAt = (z) => { + refan reads (buildPiece), so every cell that came out closed is built as before. + The bottom face has the same list for the same refan (footCorners, #92), and keeps + wallsAt's for its first fan, which every cell that comes out closed is built with. + `cutter` is another cutter's, a corner boss's (#91). */ + const cornersAt = (z, cutter = cellFastener) => { const ends = new Map(); // a wall end, to the directions of the walls that end there - for (const p of cellFastener || []) { + for (const p of cutter || []) { const zs = p.verts.map((v) => v[2]); if (!(Math.min(...zs) < z && Math.max(...zs) > z) || Math.abs(p.plane.n[2]) > 1e-9) continue; const d = [-p.plane.n[1], p.plane.n[0]]; // along the wall @@ -4810,7 +4814,7 @@

Your drawers

return [...ends.values()].filter(({ dirs }) => dirs.some((a) => dirs.some((b) => Math.abs(a[0]*b[1] - a[1]*b[0]) > 1e-6))).map((e) => e.at); }; - const fastenerFoot = wallsAt(0), fastenerTop = pad > 0.01 ? cornersAt(pad) : []; + const fastenerFoot = wallsAt(0), footCorners = cornersAt(0), fastenerTop = pad > 0.01 ? cornersAt(pad) : []; // ---- connectors ---- const conn = pieceConnectors(cfg, layout, piece); @@ -5430,12 +5434,17 @@

Your drawers

// the mounting cutters' walls, and with more room, of the joint's const avoid = halfX || halfY ? [] : [-1, 1].flatMap((sx) => [-1, 1].flatMap((sy) => fastenerFoot.map(([u, v]) => [cx + sx*cfg.holeOffset + u, cy + sy*cfg.holeOffset + v]))); + // the same with the mounting cutters' corners alone, for a cell that comes out open on + // the bottom face (below) + const footAvoid = halfX || halfY ? [] : [-1, 1].flatMap((sx) => [-1, 1].flatMap((sy) => + footCorners.map(([u, v]) => [cx + sx*cfg.holeOffset + u, cy + sy*cfg.holeOffset + v]))); for (const p of [...cuts.notch, ...cuts.key, ...cuts.puzzle]) p.verts.forEach((a, i) => { const b = p.verts[(i + 1) % p.verts.length]; if ((a[2] < 0) === (b[2] < 0)) return; const t = a[2] / (a[2] - b[2]); - avoid.push([a[0] + t*(b[0] - a[0]), a[1] + t*(b[1] - a[1]), FAN_JOINT]); + const at = [a[0] + t*(b[0] - a[0]), a[1] + t*(b[1] - a[1]), FAN_JOINT]; + avoid.push(at); footAvoid.push(at); }); // and the socket floor's, of the mounting cutters' corners there const floorAvoid = halfX || halfY ? [] : [-1, 1].flatMap((sx) => [-1, 1].flatMap((sy) => @@ -5573,6 +5582,35 @@

Your drawers

if (again && again.open < region.open && !worse(again, region)) region = again; else base = first; } + /* Still open on the bottom face, with pockets standing on it (#92): the bottom cap + fanned again clear of the mounting cutters' corners alone, and every cut taken + again on it. The first fan keeps clear of every point of their walls where they + stand on the face (wallsAt), and where two bores were unioned that is their split + points as well as their corners. A counterbore or a shank nearly as wide as the + magnet pocket it opens into is split all round: a 6 x 2 magnet from beneath over a + 3 mm hole with a 4 mm head 3 deep, at 42 mm, has 140 such points in a cell where its + corners are 56. Every point fanCentre tries had a spoke within FAN_CLEAR of one of + them, so the fan stayed at the cell's middle, with a spoke 5 microns from a corner + of a magnet pocket, and the sliver of the face between them went: 9 open edges, 6 to + 12 across the sizes that did it, with Download on. From the corners alone the fan + moves 0.1 mm and the cell closes, as the socket floor's does above (cornersAt). + Kept only if it comes out closed with nothing turned over, sharing no more edges + with the shells built beside it than the first cut; otherwise the first cut + stands. Only a cell open at the bottom face's height whose fan moves is cut again, + so a cell that comes out closed is fanned as before, and every design that builds + closed builds the same bytes. */ + if (region.open && cuts.fastener.length && openAt(region, 0)) { + const first = base; + base = directCellRegion(clipped, prof, cx, cy, H, pad, cfg.arcSegs || 6, + undefined, footAvoid, first.floorMoved ? floorAvoid : undefined); + let again = null; + if (base.footAt[0] !== first.footAt[0] || base.footAt[1] !== first.footAt[1]) + try { again = cutCell(null); } catch (e) { /* the first cut stands */ } + const near = again && !again.open && !again.turned ? builtBeside(own) : []; + if (again && !again.open && !again.turned && sharedAll(again, own, near) <= sharedAll(region, own, near)) + region = again; + else base = first; + } if (cuts.notch.length || cuts.key.length || cuts.puzzle.length) { const beside = jointCells.filter((c) => c.box[0] < x1 && x0 < c.box[2] && c.box[1] < y1 && y0 < c.box[3]); @@ -5661,6 +5699,45 @@

Your drawers

// through the top: mountLimits refuses one rather than this growing past it const bossH = bossHeight(cfg); const bossFastener = fastenerCutter(cfg, bossH - cfg.magnetH, bossH + 0.5, bossH + 0.5); + /* A boss's top, fanned again where its pocket comes out open after every try (#91). + extrudePoly fans a convex cap from one corner of its outline (the ear clip takes + the ear at its first point every time), and for half the bosses that is the cell's + corner, with a spoke to each point of the rounded inner corner. A screw's shank or a + pocket from above opens on the top, and where a spoke passes a few microns from a + corner of its walls, the two walls meeting there cross the spoke a couple of + thousandths apart, the weld takes the crossings for one point, and the sliver of + top between them and the corner goes: 1.3 microns from a 2 mm shank's corner at + 34.32 mm, 5.3 from a corner of a 4 or 6 mm magnet's pocket from above at 34.73 and + 38.95 mm. cutAgain's tries closed one boss of the two and left the other with 3 open + edges, a cell, with Download on. The cap is convex, so it can be fanned from any + point inside it: it is fanned again from the one fanCentre picks clear of the + corners of the cutter's walls where they stand at the boss's top (cornersAt), and + the pocket cut again on it, and that is kept if it comes out closed with nothing + turned over, sharing no more edges with the shells built beside it than the first + cut. A boss that comes out closed is built as before, fanned from its corner, and + so is every boss of a design that builds closed. `cut` cuts the new prism as the + first was cut. */ + const refanTop = (solid, outline, z, cutter, sites, cut) => { + const corners = cornersAt(z, cutter); + const c = fanCentre(outline, sites.flatMap((bs) => + corners.map(([u, v]) => [bs.ccx + bs.sx*off + u, bs.ccy + bs.sy*off + v]))); + const ccw = polyArea2D(outline) < 0 ? outline.slice().reverse() : outline; + const prism = extrudePoly(outline, 0, z).filter((p) => p.plane.n[2] < 0.5); // all but the top + ccw.forEach((p, i) => { + const q = ccw[(i + 1) % ccw.length], tri = makePoly([[c[0], c[1], z], [p[0], p[1], z], [q[0], q[1], z]]); + if (tri) prism.push(tri); + }); + let again; + try { again = cut(prism); } catch (e) { return solid; } // the first cut stands + if (!again || again.open || again.turned) return solid; + const box = flatBox(solid), near = []; + for (const s of shells) { + const b = flatBox(s); + if (b[0] <= box[2] + 1e-3 && box[0] <= b[2] + 1e-3 && b[1] <= box[3] + 1e-3 && box[1] <= b[3] + 1e-3) + near.push({ box: b, polys: s }); + } + return sharedAll(again, box, near) <= sharedAll(solid, box, near) ? again : solid; + }; /* The joint's cuts from beneath are taken out of the bosses as well, after their pockets: the same solids the cells are cut with. A wall key's recess, an H-clip's and a dovetail's notch sit where four cells meet, which is where four bosses meet, @@ -5754,6 +5831,11 @@

Your drawers

const pocket = movePolys(bossFastener, ccx + sx*off, ccy + sy*off); const first = csgSubtract(boss, pocket); boss = first.open ? cutAgain(boss, pocket, first) : first; + if (boss.open) // its top fanned again (refanTop) + boss = refanTop(boss, world, bossH, bossFastener, [all[n]], (prism) => { + const again = csgSubtract(prism, pocket); + return again.open ? cutAgain(prism, pocket, again) : again; + }); } shells.push(boss); continue; diff --git a/sw.js b/sw.js index 09c78df..cbc9387 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 = "9c3c3ab35a1f"; +const VERSION = "15ee9effb8b0"; const FILES = [ "./", "bins/", From 6d514da36a3d5da74df51c544b9abcc9c6737791 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 22:35:05 +0000 Subject: [PATCH 4/8] Corner pockets with a magnet from beneath no longer come out open on a boss's underside A boss's underside is fanned from its corner the same way as its top, and a 4.9 mm magnet from beneath at 43.9 mm left 3 open edges a cell there. A boss still open after its tries now has whichever cap it is open on, top or underside or both, fanned again clear of its cutter's corners at that height, kept on the same terms as before. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 11 +++++++---- src/core.js | 39 +++++++++++++++++++++++++-------------- test/plate-audit.js | 15 +++++++++------ 3 files changed, 41 insertions(+), 24 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index fa8dbc9..8b633d4 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -933,11 +933,14 @@ currently installed): first point every time), which for half the bosses is the cell's corner. A spoke from there passed 1.3 microns from a corner of a 2 mm shank opening on the top at 34.32 mm, and 5.3 from a corner of a magnet's pocket from above at 34.73 and 38.95 mm, and the - sliver went: 3 open edges a cell that none of `cutAgain`'s tries closed. The cap is - convex, so any point inside it will do: a boss still open after those tries has its top + sliver went: 3 open edges a cell that none of `cutAgain`'s tries closed. The underside + is fanned the same way, and a 4.9 mm magnet from beneath at 43.9 mm opened there. The + cap is convex, so any point inside it will do: a boss still open after those tries, + with an open edge at its top's height or its underside's, has that cap (or both) fanned again from the point `fanCentre` picks clear of the corners of its cutter's - walls at the top (`refanTop`), the pocket cut again, and the result kept on the same - terms as the bottom face's. A boss that comes out closed keeps the fan from its corner. + walls at that height (`refanCaps`), the pocket cut again, and the result kept on the + same terms as the bottom face's. A boss that comes out closed keeps the fans from its + corner. Only the bosses no joint reaches are refanned: over 3,824 designs with each joint that reaches the bosses, two pieces from 34 to 46.65 mm every 0.05, with screws, a magnet from beneath and screws, or a magnet from above, each at its cap, none of the 6,884 diff --git a/src/core.js b/src/core.js index cfc9329..caa106a 100644 --- a/src/core.js +++ b/src/core.js @@ -4305,7 +4305,8 @@ function buildPiece(cfg, layout, piece, onStatus) { // through the top: mountLimits refuses one rather than this growing past it const bossH = bossHeight(cfg); const bossFastener = fastenerCutter(cfg, bossH - cfg.magnetH, bossH + 0.5, bossH + 0.5); - /* A boss's top, fanned again where its pocket comes out open after every try (#91). + /* A boss's top or underside, fanned again where its pocket comes out open there after + every try (#91). extrudePoly fans a convex cap from one corner of its outline (the ear clip takes the ear at its first point every time), and for half the bosses that is the cell's corner, with a spoke to each point of the rounded inner corner. A screw's shank or a @@ -4321,18 +4322,28 @@ function buildPiece(cfg, layout, piece, onStatus) { the pocket cut again on it, and that is kept if it comes out closed with nothing turned over, sharing no more edges with the shells built beside it than the first cut. A boss that comes out closed is built as before, fanned from its corner, and - so is every boss of a design that builds closed. `cut` cuts the new prism as the - first was cut. */ - const refanTop = (solid, outline, z, cutter, sites, cut) => { - const corners = cornersAt(z, cutter); - const c = fanCentre(outline, sites.flatMap((bs) => - corners.map(([u, v]) => [bs.ccx + bs.sx*off + u, bs.ccy + bs.sy*off + v]))); + so is every boss of a design that builds closed. The underside is a cap fanned the + same way, and a 4.9 mm magnet from beneath at 43.9 mm, its cap there, opened on it + as the one from above did on the top: each cap the boss is open on, at its height + (openAt), is fanned again, both at once where both are. `cut` cuts the new prism as + the first was cut. */ + const refanCaps = (solid, outline, h, cutter, sites, cut) => { + const zs = [h, 0].filter((z) => openAt(solid, z)); + if (!zs.length) return solid; const ccw = polyArea2D(outline) < 0 ? outline.slice().reverse() : outline; - const prism = extrudePoly(outline, 0, z).filter((p) => p.plane.n[2] < 0.5); // all but the top - ccw.forEach((p, i) => { - const q = ccw[(i + 1) % ccw.length], tri = makePoly([[c[0], c[1], z], [p[0], p[1], z], [q[0], q[1], z]]); - if (tri) prism.push(tri); - }); + const prism = extrudePoly(outline, 0, h).filter((p) => // all but the caps fanned again + !(p.plane.n[2] > 0.5 && zs.includes(h)) && !(p.plane.n[2] < -0.5 && zs.includes(0))); + for (const z of zs) { + const corners = cornersAt(z, cutter); + const c = fanCentre(outline, sites.flatMap((bs) => + corners.map(([u, v]) => [bs.ccx + bs.sx*off + u, bs.ccy + bs.sy*off + v]))); + ccw.forEach((p, i) => { + const q = ccw[(i + 1) % ccw.length]; + const tri = makePoly(z === h ? [[c[0], c[1], z], [p[0], p[1], z], [q[0], q[1], z]] + : [[c[0], c[1], z], [q[0], q[1], z], [p[0], p[1], z]]); + if (tri) prism.push(tri); + }); + } let again; try { again = cut(prism); } catch (e) { return solid; } // the first cut stands if (!again || again.open || again.turned) return solid; @@ -4437,8 +4448,8 @@ function buildPiece(cfg, layout, piece, onStatus) { const pocket = movePolys(bossFastener, ccx + sx*off, ccy + sy*off); const first = csgSubtract(boss, pocket); boss = first.open ? cutAgain(boss, pocket, first) : first; - if (boss.open) // its top fanned again (refanTop) - boss = refanTop(boss, world, bossH, bossFastener, [all[n]], (prism) => { + if (boss.open) // a cap it is open on fanned again (refanCaps) + boss = refanCaps(boss, world, bossH, bossFastener, [all[n]], (prism) => { const again = csgSubtract(prism, pocket); return again.open ? cutAgain(prism, pocket, again) : again; }); diff --git a/test/plate-audit.js b/test/plate-audit.js index d072378..62f4c5e 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -2711,12 +2711,13 @@ console.log('\ncorner bosses beside a joint cut from beneath:'); the top, the sliver between goes (#91): a 2 mm shank at 34.32 mm, 1.3 microns, alone or under a 6 mm magnet from beneath, a 4 mm magnet's pocket from above at 34.73 mm and a 6 mm one's at 38.95, 5.3 microns, each with the head at the field's cap; and - with no screw, a 4.9 mm magnet from above at 43.9 mm, its cap there. Every try of + with no screw, a 4.9 mm magnet from above at 43.9 mm, its cap there. Its underside is + fanned the same way, and the same magnet from beneath opened there. Every try of cutAgain left one boss a cell open, 3 edges, and 6 to 15 over 2 x 2, with Download - on, until a boss still open after them had its top fanned again clear of those - corners (refanTop in buildPiece). Each has to be taken and build with nothing open - and no folds; the 2 x 2 pieces keep the edges their bosses share (quarantined - above). */ + on, until a boss still open after them had the cap it is open on fanned again clear + of those corners (refanCaps in buildPiece). Each has to be taken and build with + nothing open and no folds; the 2 x 2 pieces keep the edges their bosses share + (quarantined above). */ for (const [name, n, over] of [ ['34.32 mm, 2 mm hole', 1, { pitch: 34.32, screwHoleD: 2, screwHeadD: 6.1 }], ['34.32 mm, 2 mm hole, 6 mm magnet below', 1, { pitch: 34.32, magnets: true, screwHoleD: 2, screwHeadD: 6.1 }], @@ -2726,11 +2727,13 @@ console.log('\ncorner bosses beside a joint cut from beneath:'); screwHeadD: 9.7 }], ['43.9 mm, 4.9 mm magnet above, no screw', 1, { pitch: 43.9, magnets: true, magnetD: 4.9, magnetSide: 'top', screws: false }], + ['43.9 mm, 4.9 mm magnet below, no screw', 1, { pitch: 43.9, magnets: true, magnetD: 4.9, screws: false }], ['34.32 mm, 2 mm hole, 2 x 2', 2, { pitch: 34.32, screwHoleD: 2, screwHeadD: 6.1 }], ['34.73 mm, 4 mm magnet above, 2 x 2', 2, { pitch: 34.73, magnets: true, magnetD: 4, magnetSide: 'top', screwHoleD: 2, screwHeadD: 6.5 }], ['38.95 mm, 6 mm magnet above, 2 x 2', 2, { pitch: 38.95, magnets: true, magnetSide: 'top', screwHoleD: 3, - screwHeadD: 9.7 }]]) { + screwHeadD: 9.7 }], + ['43.9 mm, 4.9 mm magnet below, 2 x 2', 2, { pitch: 43.9, magnets: true, magnetD: 4.9, screws: false }]]) { const at = { drawerW: n * over.pitch, drawerD: n * over.pitch, bedW: 400, bedD: 400, baseMode: 'bosses', connector: 'none', screws: true, ...over }; const cfg = designCfg(at), lim = G.mountLimits(cfg, G.computeLayout(cfg)); From d5c6de5024a5d188bbfbd2d7cede1a85b26c9daa Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 22:35:13 +0000 Subject: [PATCH 5/8] Rebuild the pages Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- bins/index.html | 39 +++++++++++++++++++++++++-------------- index.html | 39 +++++++++++++++++++++++++-------------- sw.js | 2 +- 3 files changed, 51 insertions(+), 29 deletions(-) diff --git a/bins/index.html b/bins/index.html index 178c490..d307057 100644 --- a/bins/index.html +++ b/bins/index.html @@ -5911,7 +5911,8 @@

Your drawers

// through the top: mountLimits refuses one rather than this growing past it const bossH = bossHeight(cfg); const bossFastener = fastenerCutter(cfg, bossH - cfg.magnetH, bossH + 0.5, bossH + 0.5); - /* A boss's top, fanned again where its pocket comes out open after every try (#91). + /* A boss's top or underside, fanned again where its pocket comes out open there after + every try (#91). extrudePoly fans a convex cap from one corner of its outline (the ear clip takes the ear at its first point every time), and for half the bosses that is the cell's corner, with a spoke to each point of the rounded inner corner. A screw's shank or a @@ -5927,18 +5928,28 @@

Your drawers

the pocket cut again on it, and that is kept if it comes out closed with nothing turned over, sharing no more edges with the shells built beside it than the first cut. A boss that comes out closed is built as before, fanned from its corner, and - so is every boss of a design that builds closed. `cut` cuts the new prism as the - first was cut. */ - const refanTop = (solid, outline, z, cutter, sites, cut) => { - const corners = cornersAt(z, cutter); - const c = fanCentre(outline, sites.flatMap((bs) => - corners.map(([u, v]) => [bs.ccx + bs.sx*off + u, bs.ccy + bs.sy*off + v]))); + so is every boss of a design that builds closed. The underside is a cap fanned the + same way, and a 4.9 mm magnet from beneath at 43.9 mm, its cap there, opened on it + as the one from above did on the top: each cap the boss is open on, at its height + (openAt), is fanned again, both at once where both are. `cut` cuts the new prism as + the first was cut. */ + const refanCaps = (solid, outline, h, cutter, sites, cut) => { + const zs = [h, 0].filter((z) => openAt(solid, z)); + if (!zs.length) return solid; const ccw = polyArea2D(outline) < 0 ? outline.slice().reverse() : outline; - const prism = extrudePoly(outline, 0, z).filter((p) => p.plane.n[2] < 0.5); // all but the top - ccw.forEach((p, i) => { - const q = ccw[(i + 1) % ccw.length], tri = makePoly([[c[0], c[1], z], [p[0], p[1], z], [q[0], q[1], z]]); - if (tri) prism.push(tri); - }); + const prism = extrudePoly(outline, 0, h).filter((p) => // all but the caps fanned again + !(p.plane.n[2] > 0.5 && zs.includes(h)) && !(p.plane.n[2] < -0.5 && zs.includes(0))); + for (const z of zs) { + const corners = cornersAt(z, cutter); + const c = fanCentre(outline, sites.flatMap((bs) => + corners.map(([u, v]) => [bs.ccx + bs.sx*off + u, bs.ccy + bs.sy*off + v]))); + ccw.forEach((p, i) => { + const q = ccw[(i + 1) % ccw.length]; + const tri = makePoly(z === h ? [[c[0], c[1], z], [p[0], p[1], z], [q[0], q[1], z]] + : [[c[0], c[1], z], [q[0], q[1], z], [p[0], p[1], z]]); + if (tri) prism.push(tri); + }); + } let again; try { again = cut(prism); } catch (e) { return solid; } // the first cut stands if (!again || again.open || again.turned) return solid; @@ -6043,8 +6054,8 @@

Your drawers

const pocket = movePolys(bossFastener, ccx + sx*off, ccy + sy*off); const first = csgSubtract(boss, pocket); boss = first.open ? cutAgain(boss, pocket, first) : first; - if (boss.open) // its top fanned again (refanTop) - boss = refanTop(boss, world, bossH, bossFastener, [all[n]], (prism) => { + if (boss.open) // a cap it is open on fanned again (refanCaps) + boss = refanCaps(boss, world, bossH, bossFastener, [all[n]], (prism) => { const again = csgSubtract(prism, pocket); return again.open ? cutAgain(prism, pocket, again) : again; }); diff --git a/index.html b/index.html index 1e4d0a5..c486918 100644 --- a/index.html +++ b/index.html @@ -5699,7 +5699,8 @@

Your drawers

// through the top: mountLimits refuses one rather than this growing past it const bossH = bossHeight(cfg); const bossFastener = fastenerCutter(cfg, bossH - cfg.magnetH, bossH + 0.5, bossH + 0.5); - /* A boss's top, fanned again where its pocket comes out open after every try (#91). + /* A boss's top or underside, fanned again where its pocket comes out open there after + every try (#91). extrudePoly fans a convex cap from one corner of its outline (the ear clip takes the ear at its first point every time), and for half the bosses that is the cell's corner, with a spoke to each point of the rounded inner corner. A screw's shank or a @@ -5715,18 +5716,28 @@

Your drawers

the pocket cut again on it, and that is kept if it comes out closed with nothing turned over, sharing no more edges with the shells built beside it than the first cut. A boss that comes out closed is built as before, fanned from its corner, and - so is every boss of a design that builds closed. `cut` cuts the new prism as the - first was cut. */ - const refanTop = (solid, outline, z, cutter, sites, cut) => { - const corners = cornersAt(z, cutter); - const c = fanCentre(outline, sites.flatMap((bs) => - corners.map(([u, v]) => [bs.ccx + bs.sx*off + u, bs.ccy + bs.sy*off + v]))); + so is every boss of a design that builds closed. The underside is a cap fanned the + same way, and a 4.9 mm magnet from beneath at 43.9 mm, its cap there, opened on it + as the one from above did on the top: each cap the boss is open on, at its height + (openAt), is fanned again, both at once where both are. `cut` cuts the new prism as + the first was cut. */ + const refanCaps = (solid, outline, h, cutter, sites, cut) => { + const zs = [h, 0].filter((z) => openAt(solid, z)); + if (!zs.length) return solid; const ccw = polyArea2D(outline) < 0 ? outline.slice().reverse() : outline; - const prism = extrudePoly(outline, 0, z).filter((p) => p.plane.n[2] < 0.5); // all but the top - ccw.forEach((p, i) => { - const q = ccw[(i + 1) % ccw.length], tri = makePoly([[c[0], c[1], z], [p[0], p[1], z], [q[0], q[1], z]]); - if (tri) prism.push(tri); - }); + const prism = extrudePoly(outline, 0, h).filter((p) => // all but the caps fanned again + !(p.plane.n[2] > 0.5 && zs.includes(h)) && !(p.plane.n[2] < -0.5 && zs.includes(0))); + for (const z of zs) { + const corners = cornersAt(z, cutter); + const c = fanCentre(outline, sites.flatMap((bs) => + corners.map(([u, v]) => [bs.ccx + bs.sx*off + u, bs.ccy + bs.sy*off + v]))); + ccw.forEach((p, i) => { + const q = ccw[(i + 1) % ccw.length]; + const tri = makePoly(z === h ? [[c[0], c[1], z], [p[0], p[1], z], [q[0], q[1], z]] + : [[c[0], c[1], z], [q[0], q[1], z], [p[0], p[1], z]]); + if (tri) prism.push(tri); + }); + } let again; try { again = cut(prism); } catch (e) { return solid; } // the first cut stands if (!again || again.open || again.turned) return solid; @@ -5831,8 +5842,8 @@

Your drawers

const pocket = movePolys(bossFastener, ccx + sx*off, ccy + sy*off); const first = csgSubtract(boss, pocket); boss = first.open ? cutAgain(boss, pocket, first) : first; - if (boss.open) // its top fanned again (refanTop) - boss = refanTop(boss, world, bossH, bossFastener, [all[n]], (prism) => { + if (boss.open) // a cap it is open on fanned again (refanCaps) + boss = refanCaps(boss, world, bossH, bossFastener, [all[n]], (prism) => { const again = csgSubtract(prism, pocket); return again.open ? cutAgain(prism, pocket, again) : again; }); diff --git a/sw.js b/sw.js index cbc9387..f993757 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 = "15ee9effb8b0"; +const VERSION = "2482add8b9a7"; const FILES = [ "./", "bins/", From d97a75da0ee20976c8b3be327cf3fc9d0d1fd49d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 00:07:59 +0000 Subject: [PATCH 6/8] ENGINE.md: the build time a refan costs, as measured over the sweeps Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index 8b633d4..b44baf5 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -927,7 +927,7 @@ currently installed): close, and of 2,851 heads under a 6 × 2 magnet from beneath with a 3 mm hole 3 deep, 570 of 571; the 3.909 mm head's 12 open edges are on the magnet pocket's roof, 2 mm up, not on the bottom face, so it is not refanned. A design with a cell refanned takes - about 11% longer to build. + about 10% longer to build (689 such designs, 158 s in all on main, 175 s here). A corner boss's top is a cap as well, and is refanned the same way (#91). `extrudePoly` fans a convex cap from one corner of its outline (the ear clip takes the ear at its first point every time), which for half the bosses is the cell's corner. A spoke from @@ -946,7 +946,8 @@ currently installed): from beneath and screws, or a magnet from above, each at its cap, none of the 6,884 bosses a joint's cut reaches came out open, so such a boss is built as it was. Of the 47,416 others, 15 were open, five designs at 43.9 mm with a 4.9 mm magnet from above - (its cap there), and each is closed by its refan. + (its cap there), and each is closed by its refan. A design with a boss refanned takes + about 8% longer to build (17 such designs, 2.3 s in all on main, 2.5 s here). - **A pocket that reaches a joint's cut goes bad where the two ceilings are level.** A key's recess, an H-clip's and a tab's notch are all cut up from under the plate, as a magnet pocket from beneath and a counterbore are. Run into one, the pocket comes out From 3e361e3a424d6750c45ab8e34b176c7931986a09 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 00:40:48 +0000 Subject: [PATCH 7/8] ENGINE.md: say how far the chords' sag can put a bore under MOUNT_WALL, and that no audit row asserts the joint half of the corner test Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- docs/ENGINE.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/ENGINE.md b/docs/ENGINE.md index d7f709d..3f547a2 100644 --- a/docs/ENGINE.md +++ b/docs/ENGINE.md @@ -513,8 +513,11 @@ margins of 0.2 mm and a 1.3 mm magnet, and at 33 mm with the largest corner and magnet). So a boss is cut only where each of its bores, out to its widest corner, stands `MOUNT_WALL` inside the arc as well, measured to the circle the chords are drawn on (a site at the arc's centre keeps the wall it has to the cell's edges, though the chords sag -0.006 mm inside the circle at a 2 mm corner), and where no joint's cut comes within -`MOUNT_WALL` of the corner's square (none does, see below; the test stands guard). In the +0.006 mm inside the circle at a 2 mm corner, 0.012 mm at the default 4 mm and 0.021 mm at +the 6.93 mm the page allows at most, so a bore's corner can stand that much under +`MOUNT_WALL` from the outline: no open edge, fold or window comes of it), and where no +joint's cut comes within `MOUNT_WALL` of the corner's square (none does on any design the +page takes, see below; the audit has no row asserting it). In the review's sweeps (7,649 designs, 5,952 built, 3,737 with a boss out by 0.01 mm or more) that kept the square boss on every corner of 268 and on some corners of 91 more, at pitches of 29.5 to 37 mm with a corner of 3 mm or more, mostly with no margin and a bore at its cap, From fa1b95f58d4dea20c8c3b66e736593bf5426b932 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 00:56:31 +0000 Subject: [PATCH 8/8] plate-audit: the leftovers of #80 are pinned by the rows that follow Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Uqqb1cvhXxrpb3P6y6ykBY --- test/plate-audit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/plate-audit.js b/test/plate-audit.js index 43701ef..2af48a1 100644 --- a/test/plate-audit.js +++ b/test/plate-audit.js @@ -2743,8 +2743,8 @@ console.log('\ncorner bosses beside a joint cut from beneath:'); to be refused and built at the cap. The same spoke meets a corner at other pitches, and corner pockets still leave 3 open edges a cell at three of them with a 2 mm hole: 34.32 mm, with screws alone or under a 6 mm magnet from beneath; 34.73 mm, under a 4 mm magnet - from above; and with a 3 mm hole 38.95 mm, under a 6 mm magnet from above. They are what - is left of #80, open, and are not pinned here. */ + from above; and with a 3 mm hole 38.95 mm, under a 6 mm magnet from above. They were what + was left of #80, open on main; the rows for them (#91) come after the next one. */ for (const [name, head, why] of [ ['#80: 34.04 mm, 2 mm hole, 5.8 head', 5.8, false], ['#80: 34.04 mm, 2 mm hole, 5.81 head', 5.81, true],