diff --git a/bins/index.html b/bins/index.html
index b4bf8ae..e80ab2e 100644
--- a/bins/index.html
+++ b/bins/index.html
@@ -3329,7 +3329,10 @@
Your drawers
* one question is two too many, and the third is always the one nobody updates.
*
* Returns the operations for one site: `cut` — ONE closed cutter solid to subtract — and
- * `add`, closed shells to stand in the cavity it leaves.
+ * `add`, closed shells to stand in the cavity it leaves. A housing put in from above also
+ * says where it stands, for mountLimits to keep the mounting pockets out of it (#75):
+ * `footprint`, the outline everything it cuts or builds stays inside, and `bottom`, the
+ * underside of the floor it builds under the key, the lowest it reaches.
*
* The top-inserted housings used to return a third thing: convex prisms for
* clipConvexPrismTop, which peeled the material above a z-plane off whatever it was
@@ -3351,7 +3354,7 @@ Your drawers
function keySiteOps(kind, shape, prm, clr, edge, e, s, H) {
if (kind === 'snaptop') {
const p = snapTopPrm(clr);
- const { env } = snapTopParts(edge, e, s, p, H);
+ const { env, boxes } = snapTopParts(edge, e, s, p, H);
const g2 = (edge === '+x' || edge === '+y') ? -1 : 1;
const ss2 = s + 0.0017;
const rect = (edge === '+x' || edge === '-x')
@@ -3359,8 +3362,13 @@ Your drawers
[e + g2*env[1], ss2 + env[3]], [e + g2*env[0], ss2 + env[3]]]
: [[ss2 + env[2], e + g2*env[0]], [ss2 + env[3], e + g2*env[0]],
[ss2 + env[3], e + g2*env[1]], [ss2 + env[2], e + g2*env[1]]];
+ // the slot's rectangle out to the walls round it, open to the seam as the cut is
+ const t = Math.max(...boxes.map((b) => b[3])), d = Math.max(...boxes.map((b) => b[1]));
+ const footprint = [[env[0], -t], [d, -t], [d, t], [env[0], t]].map(([u, v]) =>
+ edge === '+x' || edge === '-x' ? [e + g2*u, ss2 + v] : [ss2 + v, e + g2*u]);
return { cut: extrudePoly(rect, env[4], H + 0.5),
- add: snapTopPocket(edge, e, s, p, H) };
+ add: snapTopPocket(edge, e, s, p, H),
+ footprint, bottom: Math.min(...boxes.map((b) => b[4])) };
}
if (kind === 'cup') {
const zf = Math.max(1.2, H - 2.25), zTop = H - 0.85;
@@ -3370,7 +3378,8 @@ Your drawers
decomposition, so the cavity is parallel to the wall it houses everywhere instead
of squaring off across the dogbone's taper. */
return { cut: extrudePoly(keyHalf(shape, edge, e, s, prm, clr + 0.55), zf - 0.55, H + 0.5),
- add: topPocketCup(shape, edge, e, s, prm, clr, zf, zTop) };
+ add: topPocketCup(shape, edge, e, s, prm, clr, zf, zTop),
+ footprint: keyHalf(shape, edge, e, s, prm, clr + 0.6), bottom: zf - 0.6 };
}
/* Bottom-inserted recess. The depth is prm.depth capped clear of the plate top, which
is what every configuration was already cutting: 2.3 for the H-clip, 2.0 for both
@@ -4108,10 +4117,25 @@ Your drawers
notch a dovetail or puzzle tab fits into (tabNotch). Each starts under the plate, as
a magnet pocket from beneath, a counterbore and a shank do, so any of those that
reaches one in plan has broken into it. A magnet from above meets it unless the
- pocket's floor stands at least MOUNT_SKIN over the cut's ceiling. The housings a key
- is dropped into from above (keySiteOps' 'cup' and 'snaptop') start 1.3 mm or more
- over the socket floor: a pocket from above stops under them, and a shank, which
- stays on the socket floor, is in the socket's open air up there.
+ pocket's floor stands at least MOUNT_SKIN over the cut's ceiling.
+
+ The housing a key is put into from above (keySiteOps' 'cup' and 'snaptop') is a
+ cavity cut and a cup built in it: a floor 0.6 mm thick under the key, its underside
+ 1.4 mm over the socket floor (1.3 for a snap clip), and walls from there up, at a
+ corner where four cells meet. Over a solid floor nothing reaches it: a pocket from
+ above stops under it, and a shank, which stays on the socket floor, is in the
+ socket's open air up there. A corner boss stands on the bed, though, with its
+ pocket's mouth at the bed or at the boss's top (bossHeight, at most 2.6 mm), and the
+ housing is made after the pockets are cut, so it stood in any it reached (#75): a
+ wall bowtie at 36.13 mm left 1.3 mm³ of it in the four 6 mm magnet pockets beside
+ the seam, in the top 0.6 mm of each, and 1.6 mm³ in 9.3 mm ones at 39.46 mm, and a
+ 7.9 mm magnet 1.45 mm deep, level with the cavity's floor, left 13 edges open a
+ piece, all with Download on. So it counts as a cut that stands from its underside
+ up ('housing'), as wide as all it cuts or builds: a pocket from beneath clears it by
+ stopping MOUNT_LEVEL under that, and a pocket from above and a shank, which go in
+ through it, meet any they reach. Keys in the wall reach a boss's pocket up to
+ 40 mm, the H-clip up to 38.5 and a snap clip in the wall up to 35; nothing over a
+ solid floor, and nothing else.
Measured off the solids buildPiece cuts, at the sites it cuts pockets at: whole cells
only, so a housing beside a half cell or a margin is measured from the whole cell on
@@ -4188,8 +4212,11 @@ Your drawers
const shapeOf = (edge) => {
if (shapes[edge]) return shapes[edge];
const at = { edge, e: 0, s: 0 };
- const solid = plan.kind === 'recess'
- ? keySiteOps(plan.kind, plan.shape, plan.prm, plan.clr, edge, 0, 0, H).cut
+ // a housing a key is put into from above, from its underside up through the plate
+ const housing = plan.kind === 'cup' || plan.kind === 'snaptop'
+ ? keySiteOps(plan.kind, plan.shape, plan.prm, plan.clr, edge, 0, 0, H) : null;
+ const solid = housing ? extrudePoly(housing.footprint, housing.bottom, H + 0.5)
+ : plan.kind === 'recess' ? keySiteOps(plan.kind, plan.shape, plan.prm, plan.clr, edge, 0, 0, H).cut
: tabNotch(cfg, at, pad);
const segs = [], bb = [Infinity, Infinity, -Infinity, -Infinity];
let ceiling = -Infinity;
@@ -4203,12 +4230,13 @@ Your drawers
const a = p.verts[0], b = p.verts.find((v) => Math.hypot(v[0] - a[0], v[1] - a[1]) > 1e-9);
if (b) segs.push([a, b]);
}
- const part = plan.kind === 'recess' ? keyHalf(plan.shape, edge, 0, 0, plan.prm, 0)
+ const part = plan.kind === 'recess' || housing ? keyHalf(plan.shape, edge, 0, 0, plan.prm, 0)
: cfg.connector === 'puzzle' ? puzzleShape(OPP[edge], 0, 0, cfg.puzzle, 0, false)
: tabFootprint(OPP[edge], 0, 0, cfg.tab.wr, cfg.tab.wt, cfg.tab.dp, 0.8);
// the ceiling to the micron: a notch's is 2.2 + 0.2, 2.4000000000000004 as summed
const roof = Math.round(ceiling * 1e6) / 1e6;
- return (shapes[edge] = { segs, bb, ceiling: roof, part: ring(part) });
+ const bottom = housing ? Math.round(housing.bottom * 1e6) / 1e6 : undefined;
+ return (shapes[edge] = { segs, bb, ceiling: roof, bottom, part: ring(part) });
};
// how far a point is from the footprint, less than none inside it
const from = (segs, x, y) => {
@@ -4223,11 +4251,12 @@ Your drawers
};
for (const piece of layout.pieces) {
const conn = pieceConnectors(cfg, layout, piece);
- // the cuts buildPiece makes in this piece: notches for tabs, recesses for keys
- const cuts = [...conn.notches, ...conn.pnotches, ...(plan.kind !== 'recess' ? []
- : conn.keyed.filter((bo) => !(plan.junction && offJunction(bo, cfg.pitch, piece))))];
+ // the cuts buildPiece makes in this piece: notches for tabs, recesses for keys, and
+ // the housings of keys put in from above
+ const cuts = [...conn.notches, ...conn.pnotches,
+ ...conn.keyed.filter((bo) => !(plan.junction && offJunction(bo, cfg.pitch, piece)))];
for (const bo of cuts) {
- const { segs, bb, ceiling, part } = shapeOf(bo.edge);
+ const { segs, bb, ceiling, bottom, part } = shapeOf(bo.edge);
const across = bo.edge === '+x' || bo.edge === '-x';
const ox = across ? bo.e : bo.s, oy = across ? bo.s : bo.e;
// the whole cells with a site in reach of it (a half cell has none; see buildPiece)
@@ -4247,7 +4276,7 @@ Your drawers
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) } });
}
- if (sites.length) jointCuts.push({ top: ceiling, sites });
+ if (sites.length) jointCuts.push({ top: ceiling, bottom, sites });
}
}
}
@@ -4292,13 +4321,20 @@ Your drawers
for (const c of jointCuts) {
/* a pocket from above clears a cut whose ceiling it leaves a layer of floor over; in
a corner boss its floor is the pocket's depth under the boss's top */
- if (above && !(c.top + MOUNT_SKIN > (bosses ? bossHeight(cfg) : pad) - cfg.magnetH + 1e-6)) continue;
+ if (above && c.bottom === undefined &&
+ !(c.top + MOUNT_SKIN > (bosses ? bossHeight(cfg) : pad) - cfg.magnetH + 1e-6)) continue;
+ /* a housing a key is put into from above stands from its underside (`bottom`) up: a
+ pocket from beneath clears it by stopping MOUNT_LEVEL under that, to the micron as
+ level is below. One from above takes its magnet in through it, and a shank goes up
+ through it, so each meets one it reaches. */
+ if (c.bottom !== undefined && !above &&
+ Math.round((c.bottom - ceiling) * 1e6) / 1e6 >= MOUNT_LEVEL) continue;
/* Level within MOUNT_LEVEL, measured to the micron, so a pocket that far above the
notch's ceiling and one that far below it are taken alike: 2.35 and 2.45 mm deep
against a notch 2.4 mm high, which both build clean. Unrounded, 2.45 counted as
level, and with the ceiling alone rounded 2.35 would. */
const level = Math.round(Math.abs(ceiling - c.top) * 1e6) / 1e6 < MOUNT_LEVEL;
- const stop = above ? 'floor' : !notch ? 'cut' : level ? 'level' : 'part';
+ const stop = c.bottom !== undefined ? 'housing' : above ? 'floor' : !notch ? 'cut' : level ? 'level' : 'part';
for (const s of c.sites) {
const short = fits(s.bore[sides] - seam);
const size = stop === 'part' ? 2 * s.body : short;
@@ -5362,6 +5398,33 @@ Your drawers
!others.some((d) => touchesBuilt(again, c.box, d))) { shells[c.i] = again; c.alt = alt; break; }
}
}
+ /* And if it touches still, the joint is cut again with its cutters moved a NUDGE
+ along one diagonal and then the other, as cutAgain moves them, and the first
+ that is closed, has nothing turned over and shares no edge is kept.
+ TOUCH_TRIES only reorder the cut, and a split that comes from a plane of the
+ cell's own can land in the same place every time. Puzzle tabs at a 0.35 mm fit
+ clearance, four pieces meeting, left one edge used four times in two of them at
+ every pitch from 31.6 to 52.2 mm (#72): the plane of a socket wall's facet at a
+ cell's corner, carried across the cell by the BSP, crossed the notch lobe's face
+ beside its pole 0.3 microns from where the side of the region across the
+ junction crosses it, and the two shells shared the edge there. The lobe moves
+ with the clearance and the facet with the rim cutoff and the tolerance, so it is
+ one clearance for each: 0.349 and 0.35 at the defaults, 0.608 and 0.609 tight,
+ 0.843 and 0.844 with a 1 mm rim cutoff, and 0.102, 0.103 and 0.279 to 0.281
+ with 0.1. Moved 1.7 microns, the face is crossed somewhere else. A cell that
+ touches nothing, or that the tries above clear, is built as before. One with
+ mounting pockets that still touched here was sometimes cleared later, by settle
+ cutting its pockets again; this clears it first, so such a plate comes out with
+ other bytes than before, and as closed. */
+ if (touching.length && beside.some((c) => touchesBuilt(region, own, c)))
+ for (const [dx, dy] of [[NUDGE, NUDGE], [-NUDGE, NUDGE]]) {
+ const alt = (solid, cut) => csgSubtract(solid, movePolys(cut, dx, dy));
+ let again;
+ try { again = cutCell(alt); } catch (e) { continue; }
+ if (again && !worse(again, region) && !beside.some((c) => touchesBuilt(again, own, c))) {
+ region = again; cell.alt = alt; break;
+ }
+ }
cell.joint = cutCell;
jointCells.push(cell);
}
diff --git a/docs/ENGINE.md b/docs/ENGINE.md
index cfbf5ea..76fe938 100644
--- a/docs/ENGINE.md
+++ b/docs/ENGINE.md
@@ -825,8 +825,29 @@ currently installed):
3 mm thick, is taken: its pocket is 0.04 to 0.09 mm into the notch and the magnet
0.24 mm clear of the tab. 2.4 mm thick it is level, built 96 bad edges, and is refused.
- **A pocket from above** meets any of them unless its floor stands a layer over the
- cut's ceiling. The housings a key is dropped into from above start 1.3 mm over the
- socket floor and nothing reaches them.
+ cut's ceiling.
+ - **The housing a key is put into from above is cut, then built** (`keySiteOps`' cup
+ and snap slot): a cavity is cut and a cup stood in it, a floor 0.6 mm thick under the
+ key, its underside 1.4 mm over the socket floor (1.3 for a snap clip), and walls from
+ there up, at a corner where four cells meet. Over a solid floor nothing reaches it. A
+ corner boss's pockets do, and the housing is made after them, so it stood in them
+ with Download on (#75): a wall bowtie at 36.13 mm left 1.3 mm³ in the four 6 mm
+ magnet pockets beside the seam, in the top 0.6 mm of each, and a 7.9 mm magnet there
+ 1.45 mm deep, level with the cavity's floor, left 13 edges open a piece. `mountLimits`
+ counts it as a cut from its underside up (`housing`), as wide as all it cuts or
+ builds: a pocket from beneath clears it by stopping `MOUNT_LEVEL` under that, and a
+ pocket from above or a shank, which go in through it, meets any it reaches. Of 17,820
+ designs (every joint, both floors, magnets from both sides and screws, three layouts,
+ 54 pitches from 30 to 52.2 mm) that changes the limits of 1,280, every one with
+ corner bosses and a key put in from above: wall keys up to 40 mm, the H-clip up to
+ 38.5 and a snap clip in the wall up to 35. Of the 1,771 caps it lowers, 1,573 stop a
+ tenth or two short of the first size that meets the housing, and no size taken meets
+ one. Where an H-clip's or a snap clip's housing reaches a pocket, the room it takes is
+ inside the socket rim's corner, which stands in that pocket already with no joint at
+ all (a boss's pocket may run under the rim, #83), so the pocket has no less room with
+ the joint than without it. The plate can still come out open there: an H-clip from
+ above at 34 mm took a 4 mm magnet from above with 4 edges open where the cavity meets
+ the pocket's wall, with Download on. So the cap is held off every housing alike.
- **A pocket in a corner boss counts the same way**, since the joint is cut from the
boss too (#70), and from above its floor is measured from the boss's top: wall keys
and the dovetail reach a boss's pocket up to about 39 mm, the H-clip up to about 34.5,
@@ -879,7 +900,17 @@ currently installed):
`touchesBuilt` compares the two shells' edges inside the band, and a jointed cell that
shares one with a jointed cell built before it is cut again (`TOUCH_TRIES`: each cutter
on its own, the cutters reversed, the faces reversed, the cutters one at a time from the
- last), or else the earlier cell is; the top-insert pass does the same. Mounting pockets
+ last), or else the earlier cell is; the top-insert pass does the same. Those only reorder
+ the cut, and a split that comes from a plane of the cell's own can land in the same
+ place every time, so a jointed cell that still shares an edge after both has its joint
+ cut again with the cutters moved a `NUDGE` along one diagonal and then the other (#72).
+ Puzzle tabs at a 0.35 mm fit clearance, four pieces meeting, left one edge used four
+ times in two of them at every pitch from 31.6 to 52.2 mm: the plane of a socket wall's
+ facet at a cell's corner crossed the notch lobe's face beside its pole 0.3 microns from
+ where the side of the region across the junction crosses it. The lobe moves with the
+ clearance and the facet with the rim cutoff and the tolerance, so it is one clearance
+ for each: 0.349 and 0.35 at the defaults, 0.608 and 0.609 tight, 0.843 and 0.844 with a
+ 1 mm rim cutoff, and 0.102, 0.103 and 0.279 to 0.281 with 0.1. Mounting pockets
do it with no joint at all: a pocket's wall, carried across its cell as a plane, splits
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
diff --git a/index.html b/index.html
index d783bd4..0df01a2 100644
--- a/index.html
+++ b/index.html
@@ -3116,7 +3116,10 @@ Your drawers
* one question is two too many, and the third is always the one nobody updates.
*
* Returns the operations for one site: `cut` — ONE closed cutter solid to subtract — and
- * `add`, closed shells to stand in the cavity it leaves.
+ * `add`, closed shells to stand in the cavity it leaves. A housing put in from above also
+ * says where it stands, for mountLimits to keep the mounting pockets out of it (#75):
+ * `footprint`, the outline everything it cuts or builds stays inside, and `bottom`, the
+ * underside of the floor it builds under the key, the lowest it reaches.
*
* The top-inserted housings used to return a third thing: convex prisms for
* clipConvexPrismTop, which peeled the material above a z-plane off whatever it was
@@ -3138,7 +3141,7 @@ Your drawers
function keySiteOps(kind, shape, prm, clr, edge, e, s, H) {
if (kind === 'snaptop') {
const p = snapTopPrm(clr);
- const { env } = snapTopParts(edge, e, s, p, H);
+ const { env, boxes } = snapTopParts(edge, e, s, p, H);
const g2 = (edge === '+x' || edge === '+y') ? -1 : 1;
const ss2 = s + 0.0017;
const rect = (edge === '+x' || edge === '-x')
@@ -3146,8 +3149,13 @@ Your drawers
[e + g2*env[1], ss2 + env[3]], [e + g2*env[0], ss2 + env[3]]]
: [[ss2 + env[2], e + g2*env[0]], [ss2 + env[3], e + g2*env[0]],
[ss2 + env[3], e + g2*env[1]], [ss2 + env[2], e + g2*env[1]]];
+ // the slot's rectangle out to the walls round it, open to the seam as the cut is
+ const t = Math.max(...boxes.map((b) => b[3])), d = Math.max(...boxes.map((b) => b[1]));
+ const footprint = [[env[0], -t], [d, -t], [d, t], [env[0], t]].map(([u, v]) =>
+ edge === '+x' || edge === '-x' ? [e + g2*u, ss2 + v] : [ss2 + v, e + g2*u]);
return { cut: extrudePoly(rect, env[4], H + 0.5),
- add: snapTopPocket(edge, e, s, p, H) };
+ add: snapTopPocket(edge, e, s, p, H),
+ footprint, bottom: Math.min(...boxes.map((b) => b[4])) };
}
if (kind === 'cup') {
const zf = Math.max(1.2, H - 2.25), zTop = H - 0.85;
@@ -3157,7 +3165,8 @@ Your drawers
decomposition, so the cavity is parallel to the wall it houses everywhere instead
of squaring off across the dogbone's taper. */
return { cut: extrudePoly(keyHalf(shape, edge, e, s, prm, clr + 0.55), zf - 0.55, H + 0.5),
- add: topPocketCup(shape, edge, e, s, prm, clr, zf, zTop) };
+ add: topPocketCup(shape, edge, e, s, prm, clr, zf, zTop),
+ footprint: keyHalf(shape, edge, e, s, prm, clr + 0.6), bottom: zf - 0.6 };
}
/* Bottom-inserted recess. The depth is prm.depth capped clear of the plate top, which
is what every configuration was already cutting: 2.3 for the H-clip, 2.0 for both
@@ -3895,10 +3904,25 @@ Your drawers
notch a dovetail or puzzle tab fits into (tabNotch). Each starts under the plate, as
a magnet pocket from beneath, a counterbore and a shank do, so any of those that
reaches one in plan has broken into it. A magnet from above meets it unless the
- pocket's floor stands at least MOUNT_SKIN over the cut's ceiling. The housings a key
- is dropped into from above (keySiteOps' 'cup' and 'snaptop') start 1.3 mm or more
- over the socket floor: a pocket from above stops under them, and a shank, which
- stays on the socket floor, is in the socket's open air up there.
+ pocket's floor stands at least MOUNT_SKIN over the cut's ceiling.
+
+ The housing a key is put into from above (keySiteOps' 'cup' and 'snaptop') is a
+ cavity cut and a cup built in it: a floor 0.6 mm thick under the key, its underside
+ 1.4 mm over the socket floor (1.3 for a snap clip), and walls from there up, at a
+ corner where four cells meet. Over a solid floor nothing reaches it: a pocket from
+ above stops under it, and a shank, which stays on the socket floor, is in the
+ socket's open air up there. A corner boss stands on the bed, though, with its
+ pocket's mouth at the bed or at the boss's top (bossHeight, at most 2.6 mm), and the
+ housing is made after the pockets are cut, so it stood in any it reached (#75): a
+ wall bowtie at 36.13 mm left 1.3 mm³ of it in the four 6 mm magnet pockets beside
+ the seam, in the top 0.6 mm of each, and 1.6 mm³ in 9.3 mm ones at 39.46 mm, and a
+ 7.9 mm magnet 1.45 mm deep, level with the cavity's floor, left 13 edges open a
+ piece, all with Download on. So it counts as a cut that stands from its underside
+ up ('housing'), as wide as all it cuts or builds: a pocket from beneath clears it by
+ stopping MOUNT_LEVEL under that, and a pocket from above and a shank, which go in
+ through it, meet any they reach. Keys in the wall reach a boss's pocket up to
+ 40 mm, the H-clip up to 38.5 and a snap clip in the wall up to 35; nothing over a
+ solid floor, and nothing else.
Measured off the solids buildPiece cuts, at the sites it cuts pockets at: whole cells
only, so a housing beside a half cell or a margin is measured from the whole cell on
@@ -3975,8 +3999,11 @@ Your drawers
const shapeOf = (edge) => {
if (shapes[edge]) return shapes[edge];
const at = { edge, e: 0, s: 0 };
- const solid = plan.kind === 'recess'
- ? keySiteOps(plan.kind, plan.shape, plan.prm, plan.clr, edge, 0, 0, H).cut
+ // a housing a key is put into from above, from its underside up through the plate
+ const housing = plan.kind === 'cup' || plan.kind === 'snaptop'
+ ? keySiteOps(plan.kind, plan.shape, plan.prm, plan.clr, edge, 0, 0, H) : null;
+ const solid = housing ? extrudePoly(housing.footprint, housing.bottom, H + 0.5)
+ : plan.kind === 'recess' ? keySiteOps(plan.kind, plan.shape, plan.prm, plan.clr, edge, 0, 0, H).cut
: tabNotch(cfg, at, pad);
const segs = [], bb = [Infinity, Infinity, -Infinity, -Infinity];
let ceiling = -Infinity;
@@ -3990,12 +4017,13 @@ Your drawers
const a = p.verts[0], b = p.verts.find((v) => Math.hypot(v[0] - a[0], v[1] - a[1]) > 1e-9);
if (b) segs.push([a, b]);
}
- const part = plan.kind === 'recess' ? keyHalf(plan.shape, edge, 0, 0, plan.prm, 0)
+ const part = plan.kind === 'recess' || housing ? keyHalf(plan.shape, edge, 0, 0, plan.prm, 0)
: cfg.connector === 'puzzle' ? puzzleShape(OPP[edge], 0, 0, cfg.puzzle, 0, false)
: tabFootprint(OPP[edge], 0, 0, cfg.tab.wr, cfg.tab.wt, cfg.tab.dp, 0.8);
// the ceiling to the micron: a notch's is 2.2 + 0.2, 2.4000000000000004 as summed
const roof = Math.round(ceiling * 1e6) / 1e6;
- return (shapes[edge] = { segs, bb, ceiling: roof, part: ring(part) });
+ const bottom = housing ? Math.round(housing.bottom * 1e6) / 1e6 : undefined;
+ return (shapes[edge] = { segs, bb, ceiling: roof, bottom, part: ring(part) });
};
// how far a point is from the footprint, less than none inside it
const from = (segs, x, y) => {
@@ -4010,11 +4038,12 @@ Your drawers
};
for (const piece of layout.pieces) {
const conn = pieceConnectors(cfg, layout, piece);
- // the cuts buildPiece makes in this piece: notches for tabs, recesses for keys
- const cuts = [...conn.notches, ...conn.pnotches, ...(plan.kind !== 'recess' ? []
- : conn.keyed.filter((bo) => !(plan.junction && offJunction(bo, cfg.pitch, piece))))];
+ // the cuts buildPiece makes in this piece: notches for tabs, recesses for keys, and
+ // the housings of keys put in from above
+ const cuts = [...conn.notches, ...conn.pnotches,
+ ...conn.keyed.filter((bo) => !(plan.junction && offJunction(bo, cfg.pitch, piece)))];
for (const bo of cuts) {
- const { segs, bb, ceiling, part } = shapeOf(bo.edge);
+ const { segs, bb, ceiling, bottom, part } = shapeOf(bo.edge);
const across = bo.edge === '+x' || bo.edge === '-x';
const ox = across ? bo.e : bo.s, oy = across ? bo.s : bo.e;
// the whole cells with a site in reach of it (a half cell has none; see buildPiece)
@@ -4034,7 +4063,7 @@ Your drawers
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) } });
}
- if (sites.length) jointCuts.push({ top: ceiling, sites });
+ if (sites.length) jointCuts.push({ top: ceiling, bottom, sites });
}
}
}
@@ -4079,13 +4108,20 @@ Your drawers
for (const c of jointCuts) {
/* a pocket from above clears a cut whose ceiling it leaves a layer of floor over; in
a corner boss its floor is the pocket's depth under the boss's top */
- if (above && !(c.top + MOUNT_SKIN > (bosses ? bossHeight(cfg) : pad) - cfg.magnetH + 1e-6)) continue;
+ if (above && c.bottom === undefined &&
+ !(c.top + MOUNT_SKIN > (bosses ? bossHeight(cfg) : pad) - cfg.magnetH + 1e-6)) continue;
+ /* a housing a key is put into from above stands from its underside (`bottom`) up: a
+ pocket from beneath clears it by stopping MOUNT_LEVEL under that, to the micron as
+ level is below. One from above takes its magnet in through it, and a shank goes up
+ through it, so each meets one it reaches. */
+ if (c.bottom !== undefined && !above &&
+ Math.round((c.bottom - ceiling) * 1e6) / 1e6 >= MOUNT_LEVEL) continue;
/* Level within MOUNT_LEVEL, measured to the micron, so a pocket that far above the
notch's ceiling and one that far below it are taken alike: 2.35 and 2.45 mm deep
against a notch 2.4 mm high, which both build clean. Unrounded, 2.45 counted as
level, and with the ceiling alone rounded 2.35 would. */
const level = Math.round(Math.abs(ceiling - c.top) * 1e6) / 1e6 < MOUNT_LEVEL;
- const stop = above ? 'floor' : !notch ? 'cut' : level ? 'level' : 'part';
+ const stop = c.bottom !== undefined ? 'housing' : above ? 'floor' : !notch ? 'cut' : level ? 'level' : 'part';
for (const s of c.sites) {
const short = fits(s.bore[sides] - seam);
const size = stop === 'part' ? 2 * s.body : short;
@@ -5149,6 +5185,33 @@ Your drawers
!others.some((d) => touchesBuilt(again, c.box, d))) { shells[c.i] = again; c.alt = alt; break; }
}
}
+ /* And if it touches still, the joint is cut again with its cutters moved a NUDGE
+ along one diagonal and then the other, as cutAgain moves them, and the first
+ that is closed, has nothing turned over and shares no edge is kept.
+ TOUCH_TRIES only reorder the cut, and a split that comes from a plane of the
+ cell's own can land in the same place every time. Puzzle tabs at a 0.35 mm fit
+ clearance, four pieces meeting, left one edge used four times in two of them at
+ every pitch from 31.6 to 52.2 mm (#72): the plane of a socket wall's facet at a
+ cell's corner, carried across the cell by the BSP, crossed the notch lobe's face
+ beside its pole 0.3 microns from where the side of the region across the
+ junction crosses it, and the two shells shared the edge there. The lobe moves
+ with the clearance and the facet with the rim cutoff and the tolerance, so it is
+ one clearance for each: 0.349 and 0.35 at the defaults, 0.608 and 0.609 tight,
+ 0.843 and 0.844 with a 1 mm rim cutoff, and 0.102, 0.103 and 0.279 to 0.281
+ with 0.1. Moved 1.7 microns, the face is crossed somewhere else. A cell that
+ touches nothing, or that the tries above clear, is built as before. One with
+ mounting pockets that still touched here was sometimes cleared later, by settle
+ cutting its pockets again; this clears it first, so such a plate comes out with
+ other bytes than before, and as closed. */
+ if (touching.length && beside.some((c) => touchesBuilt(region, own, c)))
+ for (const [dx, dy] of [[NUDGE, NUDGE], [-NUDGE, NUDGE]]) {
+ const alt = (solid, cut) => csgSubtract(solid, movePolys(cut, dx, dy));
+ let again;
+ try { again = cutCell(alt); } catch (e) { continue; }
+ if (again && !worse(again, region) && !beside.some((c) => touchesBuilt(again, own, c))) {
+ region = again; cell.alt = alt; break;
+ }
+ }
cell.joint = cutCell;
jointCells.push(cell);
}
@@ -7842,9 +7905,10 @@ Your drawers
/* Why `field` stops where it does. A joint's cut stops it in one of the ways mountLimits'
`joint` names: a pocket from beneath as deep as a dovetail's notch, a magnet or screw
that would reach the tab in one, a pocket any wider coming too near the notch's edge,
+ a hole that would meet the housing of a key put in from above ('housing'),
or any other hole breaking into the cut. The hint about keys put in from above is not
given with corner pockets: there is no solid floor under them, and such a key's cup
- stands into a boss's pocket (#75). Nor where the bosses are buried in the floor a
+ reaches a boss's pocket (#75). Nor where the bosses are buried in the floor a
joint needs (cornerBosses): a key put in from above, in the walls, needs none, so
taking the hint would stand them up again, and the cup in their pockets. Only a floor
asked for (bottomPad) keeps them buried whatever the joint. */
@@ -7858,6 +7922,11 @@ Your drawers
: joint === 'part' ? `${{ magnetD: 'a magnet', screwHeadD: 'a screw head', screwHoleD: 'a screw' }[field]} ` +
`has to stay clear of the ${CONNECTOR_NAMES[state.connector]} in the notches beside it`
: joint === 'near' ? `a pocket any wider would come too near the edge of the ${cuts} to cut cleanly`
+ /* put in from beneath, a key's or an H-clip's recess leaves more room wherever its cup
+ stops a size (all 1,508 such caps in the #75 sweep); a snap clip's reaches further */
+ : joint === 'housing' ? `a hole has to stay out of the housings the ${CONNECTOR_NAMES[state.connector]} ` +
+ 'drop into from above' + (state.connector === 'snap' ? ''
+ : `; put in from beneath, the ${CONNECTOR_NAMES[state.connector]} leave more room`)
: joint ? `a hole has to stay out of the ${cuts}`
: mount().beside[field] ? 'a cell\'s four holes have to stay clear of each other'
: cornerBosses(state) ? 'a pocket has to stay inside its corner boss'
diff --git a/src/core.js b/src/core.js
index e3dd24b..94dae66 100644
--- a/src/core.js
+++ b/src/core.js
@@ -1723,7 +1723,10 @@ function topPocketCup(type, edge, e, s, prm, clr, zf, zTop) {
* one question is two too many, and the third is always the one nobody updates.
*
* Returns the operations for one site: `cut` — ONE closed cutter solid to subtract — and
- * `add`, closed shells to stand in the cavity it leaves.
+ * `add`, closed shells to stand in the cavity it leaves. A housing put in from above also
+ * says where it stands, for mountLimits to keep the mounting pockets out of it (#75):
+ * `footprint`, the outline everything it cuts or builds stays inside, and `bottom`, the
+ * underside of the floor it builds under the key, the lowest it reaches.
*
* The top-inserted housings used to return a third thing: convex prisms for
* clipConvexPrismTop, which peeled the material above a z-plane off whatever it was
@@ -1745,7 +1748,7 @@ function topPocketCup(type, edge, e, s, prm, clr, zf, zTop) {
function keySiteOps(kind, shape, prm, clr, edge, e, s, H) {
if (kind === 'snaptop') {
const p = snapTopPrm(clr);
- const { env } = snapTopParts(edge, e, s, p, H);
+ const { env, boxes } = snapTopParts(edge, e, s, p, H);
const g2 = (edge === '+x' || edge === '+y') ? -1 : 1;
const ss2 = s + 0.0017;
const rect = (edge === '+x' || edge === '-x')
@@ -1753,8 +1756,13 @@ function keySiteOps(kind, shape, prm, clr, edge, e, s, H) {
[e + g2*env[1], ss2 + env[3]], [e + g2*env[0], ss2 + env[3]]]
: [[ss2 + env[2], e + g2*env[0]], [ss2 + env[3], e + g2*env[0]],
[ss2 + env[3], e + g2*env[1]], [ss2 + env[2], e + g2*env[1]]];
+ // the slot's rectangle out to the walls round it, open to the seam as the cut is
+ const t = Math.max(...boxes.map((b) => b[3])), d = Math.max(...boxes.map((b) => b[1]));
+ const footprint = [[env[0], -t], [d, -t], [d, t], [env[0], t]].map(([u, v]) =>
+ edge === '+x' || edge === '-x' ? [e + g2*u, ss2 + v] : [ss2 + v, e + g2*u]);
return { cut: extrudePoly(rect, env[4], H + 0.5),
- add: snapTopPocket(edge, e, s, p, H) };
+ add: snapTopPocket(edge, e, s, p, H),
+ footprint, bottom: Math.min(...boxes.map((b) => b[4])) };
}
if (kind === 'cup') {
const zf = Math.max(1.2, H - 2.25), zTop = H - 0.85;
@@ -1764,7 +1772,8 @@ function keySiteOps(kind, shape, prm, clr, edge, e, s, H) {
decomposition, so the cavity is parallel to the wall it houses everywhere instead
of squaring off across the dogbone's taper. */
return { cut: extrudePoly(keyHalf(shape, edge, e, s, prm, clr + 0.55), zf - 0.55, H + 0.5),
- add: topPocketCup(shape, edge, e, s, prm, clr, zf, zTop) };
+ add: topPocketCup(shape, edge, e, s, prm, clr, zf, zTop),
+ footprint: keyHalf(shape, edge, e, s, prm, clr + 0.6), bottom: zf - 0.6 };
}
/* Bottom-inserted recess. The depth is prm.depth capped clear of the plate top, which
is what every configuration was already cutting: 2.3 for the H-clip, 2.0 for both
@@ -2502,10 +2511,25 @@ function mountLimits(cfg, layout, known) {
notch a dovetail or puzzle tab fits into (tabNotch). Each starts under the plate, as
a magnet pocket from beneath, a counterbore and a shank do, so any of those that
reaches one in plan has broken into it. A magnet from above meets it unless the
- pocket's floor stands at least MOUNT_SKIN over the cut's ceiling. The housings a key
- is dropped into from above (keySiteOps' 'cup' and 'snaptop') start 1.3 mm or more
- over the socket floor: a pocket from above stops under them, and a shank, which
- stays on the socket floor, is in the socket's open air up there.
+ pocket's floor stands at least MOUNT_SKIN over the cut's ceiling.
+
+ The housing a key is put into from above (keySiteOps' 'cup' and 'snaptop') is a
+ cavity cut and a cup built in it: a floor 0.6 mm thick under the key, its underside
+ 1.4 mm over the socket floor (1.3 for a snap clip), and walls from there up, at a
+ corner where four cells meet. Over a solid floor nothing reaches it: a pocket from
+ above stops under it, and a shank, which stays on the socket floor, is in the
+ socket's open air up there. A corner boss stands on the bed, though, with its
+ pocket's mouth at the bed or at the boss's top (bossHeight, at most 2.6 mm), and the
+ housing is made after the pockets are cut, so it stood in any it reached (#75): a
+ wall bowtie at 36.13 mm left 1.3 mm³ of it in the four 6 mm magnet pockets beside
+ the seam, in the top 0.6 mm of each, and 1.6 mm³ in 9.3 mm ones at 39.46 mm, and a
+ 7.9 mm magnet 1.45 mm deep, level with the cavity's floor, left 13 edges open a
+ piece, all with Download on. So it counts as a cut that stands from its underside
+ up ('housing'), as wide as all it cuts or builds: a pocket from beneath clears it by
+ stopping MOUNT_LEVEL under that, and a pocket from above and a shank, which go in
+ through it, meet any they reach. Keys in the wall reach a boss's pocket up to
+ 40 mm, the H-clip up to 38.5 and a snap clip in the wall up to 35; nothing over a
+ solid floor, and nothing else.
Measured off the solids buildPiece cuts, at the sites it cuts pockets at: whole cells
only, so a housing beside a half cell or a margin is measured from the whole cell on
@@ -2582,8 +2606,11 @@ function mountLimits(cfg, layout, known) {
const shapeOf = (edge) => {
if (shapes[edge]) return shapes[edge];
const at = { edge, e: 0, s: 0 };
- const solid = plan.kind === 'recess'
- ? keySiteOps(plan.kind, plan.shape, plan.prm, plan.clr, edge, 0, 0, H).cut
+ // a housing a key is put into from above, from its underside up through the plate
+ const housing = plan.kind === 'cup' || plan.kind === 'snaptop'
+ ? keySiteOps(plan.kind, plan.shape, plan.prm, plan.clr, edge, 0, 0, H) : null;
+ const solid = housing ? extrudePoly(housing.footprint, housing.bottom, H + 0.5)
+ : plan.kind === 'recess' ? keySiteOps(plan.kind, plan.shape, plan.prm, plan.clr, edge, 0, 0, H).cut
: tabNotch(cfg, at, pad);
const segs = [], bb = [Infinity, Infinity, -Infinity, -Infinity];
let ceiling = -Infinity;
@@ -2597,12 +2624,13 @@ function mountLimits(cfg, layout, known) {
const a = p.verts[0], b = p.verts.find((v) => Math.hypot(v[0] - a[0], v[1] - a[1]) > 1e-9);
if (b) segs.push([a, b]);
}
- const part = plan.kind === 'recess' ? keyHalf(plan.shape, edge, 0, 0, plan.prm, 0)
+ const part = plan.kind === 'recess' || housing ? keyHalf(plan.shape, edge, 0, 0, plan.prm, 0)
: cfg.connector === 'puzzle' ? puzzleShape(OPP[edge], 0, 0, cfg.puzzle, 0, false)
: tabFootprint(OPP[edge], 0, 0, cfg.tab.wr, cfg.tab.wt, cfg.tab.dp, 0.8);
// the ceiling to the micron: a notch's is 2.2 + 0.2, 2.4000000000000004 as summed
const roof = Math.round(ceiling * 1e6) / 1e6;
- return (shapes[edge] = { segs, bb, ceiling: roof, part: ring(part) });
+ const bottom = housing ? Math.round(housing.bottom * 1e6) / 1e6 : undefined;
+ return (shapes[edge] = { segs, bb, ceiling: roof, bottom, part: ring(part) });
};
// how far a point is from the footprint, less than none inside it
const from = (segs, x, y) => {
@@ -2617,11 +2645,12 @@ function mountLimits(cfg, layout, known) {
};
for (const piece of layout.pieces) {
const conn = pieceConnectors(cfg, layout, piece);
- // the cuts buildPiece makes in this piece: notches for tabs, recesses for keys
- const cuts = [...conn.notches, ...conn.pnotches, ...(plan.kind !== 'recess' ? []
- : conn.keyed.filter((bo) => !(plan.junction && offJunction(bo, cfg.pitch, piece))))];
+ // the cuts buildPiece makes in this piece: notches for tabs, recesses for keys, and
+ // the housings of keys put in from above
+ const cuts = [...conn.notches, ...conn.pnotches,
+ ...conn.keyed.filter((bo) => !(plan.junction && offJunction(bo, cfg.pitch, piece)))];
for (const bo of cuts) {
- const { segs, bb, ceiling, part } = shapeOf(bo.edge);
+ const { segs, bb, ceiling, bottom, part } = shapeOf(bo.edge);
const across = bo.edge === '+x' || bo.edge === '-x';
const ox = across ? bo.e : bo.s, oy = across ? bo.s : bo.e;
// the whole cells with a site in reach of it (a half cell has none; see buildPiece)
@@ -2641,7 +2670,7 @@ function mountLimits(cfg, layout, known) {
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) } });
}
- if (sites.length) jointCuts.push({ top: ceiling, sites });
+ if (sites.length) jointCuts.push({ top: ceiling, bottom, sites });
}
}
}
@@ -2686,13 +2715,20 @@ function mountLimits(cfg, layout, known) {
for (const c of jointCuts) {
/* a pocket from above clears a cut whose ceiling it leaves a layer of floor over; in
a corner boss its floor is the pocket's depth under the boss's top */
- if (above && !(c.top + MOUNT_SKIN > (bosses ? bossHeight(cfg) : pad) - cfg.magnetH + 1e-6)) continue;
+ if (above && c.bottom === undefined &&
+ !(c.top + MOUNT_SKIN > (bosses ? bossHeight(cfg) : pad) - cfg.magnetH + 1e-6)) continue;
+ /* a housing a key is put into from above stands from its underside (`bottom`) up: a
+ pocket from beneath clears it by stopping MOUNT_LEVEL under that, to the micron as
+ level is below. One from above takes its magnet in through it, and a shank goes up
+ through it, so each meets one it reaches. */
+ if (c.bottom !== undefined && !above &&
+ Math.round((c.bottom - ceiling) * 1e6) / 1e6 >= MOUNT_LEVEL) continue;
/* Level within MOUNT_LEVEL, measured to the micron, so a pocket that far above the
notch's ceiling and one that far below it are taken alike: 2.35 and 2.45 mm deep
against a notch 2.4 mm high, which both build clean. Unrounded, 2.45 counted as
level, and with the ceiling alone rounded 2.35 would. */
const level = Math.round(Math.abs(ceiling - c.top) * 1e6) / 1e6 < MOUNT_LEVEL;
- const stop = above ? 'floor' : !notch ? 'cut' : level ? 'level' : 'part';
+ const stop = c.bottom !== undefined ? 'housing' : above ? 'floor' : !notch ? 'cut' : level ? 'level' : 'part';
for (const s of c.sites) {
const short = fits(s.bore[sides] - seam);
const size = stop === 'part' ? 2 * s.body : short;
@@ -3756,6 +3792,33 @@ function buildPiece(cfg, layout, piece, onStatus) {
!others.some((d) => touchesBuilt(again, c.box, d))) { shells[c.i] = again; c.alt = alt; break; }
}
}
+ /* And if it touches still, the joint is cut again with its cutters moved a NUDGE
+ along one diagonal and then the other, as cutAgain moves them, and the first
+ that is closed, has nothing turned over and shares no edge is kept.
+ TOUCH_TRIES only reorder the cut, and a split that comes from a plane of the
+ cell's own can land in the same place every time. Puzzle tabs at a 0.35 mm fit
+ clearance, four pieces meeting, left one edge used four times in two of them at
+ every pitch from 31.6 to 52.2 mm (#72): the plane of a socket wall's facet at a
+ cell's corner, carried across the cell by the BSP, crossed the notch lobe's face
+ beside its pole 0.3 microns from where the side of the region across the
+ junction crosses it, and the two shells shared the edge there. The lobe moves
+ with the clearance and the facet with the rim cutoff and the tolerance, so it is
+ one clearance for each: 0.349 and 0.35 at the defaults, 0.608 and 0.609 tight,
+ 0.843 and 0.844 with a 1 mm rim cutoff, and 0.102, 0.103 and 0.279 to 0.281
+ with 0.1. Moved 1.7 microns, the face is crossed somewhere else. A cell that
+ touches nothing, or that the tries above clear, is built as before. One with
+ mounting pockets that still touched here was sometimes cleared later, by settle
+ cutting its pockets again; this clears it first, so such a plate comes out with
+ other bytes than before, and as closed. */
+ if (touching.length && beside.some((c) => touchesBuilt(region, own, c)))
+ for (const [dx, dy] of [[NUDGE, NUDGE], [-NUDGE, NUDGE]]) {
+ const alt = (solid, cut) => csgSubtract(solid, movePolys(cut, dx, dy));
+ let again;
+ try { again = cutCell(alt); } catch (e) { continue; }
+ if (again && !worse(again, region) && !beside.some((c) => touchesBuilt(again, own, c))) {
+ region = again; cell.alt = alt; break;
+ }
+ }
cell.joint = cutCell;
jointCells.push(cell);
}
diff --git a/src/ui.js b/src/ui.js
index 244b710..72985d7 100644
--- a/src/ui.js
+++ b/src/ui.js
@@ -101,9 +101,10 @@ const cutsNamed = () => `${['dovetail', 'puzzle'].includes(state.connector) ? 'n
/* Why `field` stops where it does. A joint's cut stops it in one of the ways mountLimits'
`joint` names: a pocket from beneath as deep as a dovetail's notch, a magnet or screw
that would reach the tab in one, a pocket any wider coming too near the notch's edge,
+ a hole that would meet the housing of a key put in from above ('housing'),
or any other hole breaking into the cut. The hint about keys put in from above is not
given with corner pockets: there is no solid floor under them, and such a key's cup
- stands into a boss's pocket (#75). Nor where the bosses are buried in the floor a
+ reaches a boss's pocket (#75). Nor where the bosses are buried in the floor a
joint needs (cornerBosses): a key put in from above, in the walls, needs none, so
taking the hint would stand them up again, and the cup in their pockets. Only a floor
asked for (bottomPad) keeps them buried whatever the joint. */
@@ -117,6 +118,11 @@ const mountWhy = (opens, field) => {
: joint === 'part' ? `${{ magnetD: 'a magnet', screwHeadD: 'a screw head', screwHoleD: 'a screw' }[field]} ` +
`has to stay clear of the ${CONNECTOR_NAMES[state.connector]} in the notches beside it`
: joint === 'near' ? `a pocket any wider would come too near the edge of the ${cuts} to cut cleanly`
+ /* put in from beneath, a key's or an H-clip's recess leaves more room wherever its cup
+ stops a size (all 1,508 such caps in the #75 sweep); a snap clip's reaches further */
+ : joint === 'housing' ? `a hole has to stay out of the housings the ${CONNECTOR_NAMES[state.connector]} ` +
+ 'drop into from above' + (state.connector === 'snap' ? ''
+ : `; put in from beneath, the ${CONNECTOR_NAMES[state.connector]} leave more room`)
: joint ? `a hole has to stay out of the ${cuts}`
: mount().beside[field] ? 'a cell\'s four holes have to stay clear of each other'
: cornerBosses(state) ? 'a pocket has to stay inside its corner boss'
diff --git a/sw.js b/sw.js
index 661b5d4..2c0a7c0 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 = "a8545a50ba92";
+const VERSION = "63217bf3b668";
const FILES = [
"./",
"bins/",
diff --git a/test/plate-audit.js b/test/plate-audit.js
index 8c7ea47..711e609 100644
--- a/test/plate-audit.js
+++ b/test/plate-audit.js
@@ -225,6 +225,14 @@ const CASES = [
mRight: 0.5, mFront: 0.3, mBack: 1, splitMode: 'manual', rowCuts: [1, 2],
colCuts: [[], [], []], connector: 'puzzle', outerRadius: 4, arcSegs: 6,
puzzle: { ...G.DEFAULTS.puzzle, clr: 0.1 } },
+ /* Four pieces meeting at a corner with puzzle tabs at a 0.35 mm fit (#72): two of them
+ had an edge used four times beside a notch's pole, at every pitch from 31.6 to 52.2
+ mm, and 0.34 and 0.36 were clean. A socket wall's facet plane, carried across the
+ cell by the BSP, crossed the lobe's face 0.3 microns from where the side of the region
+ across the junction crosses it, and no retry that keeps the cutters where they are
+ moves that plane (the touch retries in buildPiece). */
+ { name: 'puzzle 42 quads, 0.35', pitch: 42, drawerW: 168, drawerD: 168, bedW: 100, bedD: 100,
+ connector: 'puzzle', arcSegs: 6, puzzle: { ...G.DEFAULTS.puzzle, clr: 0.35 } },
/* A skeleton plate whose far cuts moved half a thousandth, clear of a vertex 0.0995 mm
from them, so the strip its cells keep solid where the margin was ended a hair from
the margin's own region: four edges each used four times. A cut that moves now moves
@@ -2386,6 +2394,91 @@ console.log('\ncorner bosses beside a joint cut from beneath:');
`${folds ? `, ${folds} FOLDS` : ''}${good ? '' : ' FAIL'}`);
if (!good) bad++;
}
+ /* And a key put in from above (#75). Its housing is a cup the plate builds, a floor
+ 0.6 mm thick under the key 1.4 mm over the bed, with its walls from there up, at the
+ corner where four cells meet, as a boss is; and it is built after the bosses' pockets
+ are cut, so it stood in any it reached. A wall bowtie at 36.13 mm took a 6 mm magnet
+ from beneath with 1.3 mm³ of the cup in the four pockets beside the seam, in the top
+ 0.6 mm of each, with Download on. mountLimits counts the cup now ('housing'): each of
+ these is asked at the size that met it, which has to be refused for the housing, and
+ built at the size the field takes, where every pocket has to be as open as in the
+ same plate with no joint, read on vertical lines 0.1 mm apart every 0.05 mm up its
+ depth. With no joint, not the pocket's whole volume: at these pitches the socket's
+ rim stands in part of a 6 mm pocket already, joint or not. And a pocket the cup does
+ not reach, at 42 mm, has to be taken as before. */
+ {
+ const h = 0.1, dz = 0.05;
+ // the open room in each pocket of a design, in mm³, mouth to floor
+ const room = (r) => {
+ const cfg = r.cfg, out = [];
+ const top = Math.min(2.6, Math.max(cfg.magnets ? cfg.magnetH + 0.8 : 0, cfg.screws ? cfg.screwHeadDepth + 1 : 0));
+ const spans = [];
+ if (cfg.magnets) spans.push(cfg.magnetSide === 'top'
+ ? [cfg.magnetD / 2, top - cfg.magnetH, top] : [cfg.magnetD / 2, 0, cfg.magnetH]);
+ if (cfg.screws) spans.push([cfg.screwHeadD / 2, 0, cfg.screwHeadDepth], [cfg.screwHoleD / 2, 0, top]);
+ r.pieces.forEach((polys, pi) => {
+ const pc = r.L.pieces[pi], A = columns(polys);
+ for (let i = 0; i < pc.nx; i++) for (let k = 0; k < pc.ny; k++)
+ for (const sx of [-1, 1]) for (const sy of [-1, 1]) {
+ const px = pc.mL + (i + 0.5) * cfg.pitch + sx * cfg.holeOffset;
+ const py = pc.mF + (k + 0.5) * cfg.pitch + sy * cfg.holeOffset;
+ let v = 0;
+ for (const [rad, z0, z1] of spans)
+ for (let x = -rad; x <= rad; x += h) for (let y = -rad; y <= rad; y += h) {
+ if (Math.hypot(x, y) > rad - 0.02) continue;
+ const col = A(px + x + 0.000731, py + y + 0.000419);
+ for (let z = z0 + dz / 2; z < z1; z += dz) if (col(z) < 1) v += h * h * dz;
+ }
+ out.push(v);
+ }
+ });
+ return out;
+ };
+ const P = (p) => ({ pitch: p, drawerW: 4 * p, drawerD: 2 * p, bedW: 2 * p + 16, bedD: 400, baseMode: 'bosses',
+ connector: 'bowtie', keyMount: 'wall', keyInsert: 'top' });
+ for (const [name, over, f, d, why] of [
+ ['wall bowtie at 36.13 mm, 6 mm magnet', { ...P(36.13), magnets: true }, 'magnetD', 6, 'housing'],
+ ['wall bowtie at 36.13 mm, magnet above', { ...P(36.13), magnets: true, magnetSide: 'top' }, 'magnetD', 6, 'housing'],
+ ['wall bowtie at 36.13 mm, 6 mm head', { ...P(36.13), screws: true }, 'screwHeadD', 6, 'housing'],
+ ['wall puzzle key at 36.13 mm, magnet', { ...P(36.13), connector: 'puzzlekey', magnets: true }, 'magnetD', 6, 'housing'],
+ ['wall bowtie at 39.46 mm, 9.3 magnet', { ...P(39.46), magnets: true }, 'magnetD', 9.3, 'housing'],
+ ['wall bowtie at 42 mm, 6 mm magnet', { ...P(42), magnets: true }, 'magnetD', 6, false]]) {
+ const asked = designCfg({ ...over, [f]: d });
+ const lims = G.mountLimits(asked, G.computeLayout(asked));
+ const refused = d > lims[f] + 1e-9 && lims.joint[f];
+ const size = Math.min(d, lims[f]);
+ const r = buildAll({ ...over, [f]: size });
+ const got = room(r), plain = room(buildAll({ ...over, [f]: size, connector: 'none' }));
+ const short = got.map((v, k) => plain[k] - v).filter((v) => v > 0.005);
+ const good = refused === why && !short.length && !r.open;
+ console.log(` ${name.padEnd(38)} ${refused ? `refused (${refused}), built at ${size}` : 'taken'}; ` +
+ (short.length ? `${short.length} of ${got.length} pockets SHORT of their room with no joint, ` +
+ `${short.reduce((s, v) => s + v, 0).toFixed(2)} mm³` : `${got.length} pockets as open as with no joint`) +
+ `; ${leakText(r)}${good ? '' : ` FAIL${refused === why ? '' : why ? `: has to be refused for the ${why}` : ': has to be taken'}`}`);
+ if (!good) bad++;
+ }
+ /* And where the cup meets a pocket the plate can come out open, which the room above
+ does not see: an H-clip from above at 34 mm took a 4 mm magnet from above with 4
+ edges open in A1, where the cavity meets the pocket's wall, and a wall bowtie at
+ 36.13 mm a 7.9 mm magnet 1.45 mm deep, level with the cavity's floor, with 13 open
+ a piece, both with Download on. The H-clip's pocket has no less room than with no
+ joint, since the rim's corner stands where the cup does (#83), and it is refused
+ all the same. Each has to be refused for the housing, and built at the size the
+ field takes with no edge open. */
+ for (const [name, over, d] of [
+ ['H-clip at 34 mm, 4 mm magnet above', { ...P(34), connector: 'hclip', magnets: true, magnetSide: 'top' }, 4],
+ ['wall bowtie at 36.13 mm, 7.9 x 1.45', { ...P(36.13), magnets: true, magnetH: 1.45 }, 7.9]]) {
+ const asked = designCfg({ ...over, magnetD: d });
+ const lims = G.mountLimits(asked, G.computeLayout(asked));
+ const refused = d > lims.magnetD + 1e-9 && lims.joint.magnetD;
+ const size = Math.min(d, lims.magnetD);
+ const r = buildAll({ ...over, magnetD: size });
+ const good = refused === 'housing' && !r.open;
+ console.log(` ${name.padEnd(38)} ${refused ? `refused (${refused}), built at ${size}` : 'TAKEN'}; ${leakText(r)}` +
+ `${good ? '' : ` FAIL${refused === 'housing' ? '' : ': has to be refused for the housing'}`}`);
+ if (!good) bad++;
+ }
+ }
}
/* The fit clearance at its ceiling, for every joint and every pitch band.
diff --git a/test/ui/plates-robustness.spec.js b/test/ui/plates-robustness.spec.js
index a50307f..9dff088 100644
--- a/test/ui/plates-robustness.spec.js
+++ b/test/ui/plates-robustness.spec.js
@@ -130,6 +130,13 @@ test.describe('ranges on the geometry fields', () => {
/Magnet Ø of 11\.93 mm is refused at a 41\.49 mm pitch — .* Use 11\.89 mm or less, 11\.92 mm, or 11\.95 mm or more\./],
['#pi=31&cl=0&mg=1&md=1', 'errMagnet',
/Magnet Ø of 1 mm is refused at a 31 mm pitch — .* Use 1\.01 mm, or 1\.04 mm or more\./],
+ /* #75: a bowtie key in the walls put in from above stands in a cup of its own, and with
+ corner bosses there is no floor between that cup and the bosses' pockets. With the
+ default 6 mm magnet at 36.13 mm the cup's floor stood in the pocket beside each key,
+ watertight and with Download on, where the magnet could not seat. */
+ ['#pi=36.13&w=144.52&d=72.26&mm=custom&ml=0&mr=0&mf=0&mb=0&bw=88.26&bd=400&cn=bowtie&km=wall&ki=top' +
+ '&bm=bosses&mg=1', 'errMagnet',
+ /Magnet Ø must be 5 mm or less at a 36\.13 mm pitch — mounting holes sit 13 mm from each cell centre, where the Gridfinity spec puts them, and a hole has to stay out of the housings the bowtie keys drop into from above; put in from beneath, the bowtie keys leave more room\./],
/* #70: the joint is cut from the corner bosses now, so a boss's pocket can reach it as
a cell's can. A bowtie key in the walls at 36.13 mm took a 7.9 mm magnet, and with
the recess cut from the bosses it would have built 19 open edges a piece. */
@@ -218,6 +225,31 @@ test.describe('ranges on the geometry fields', () => {
expect(await text(page, 'errMagnet')).toMatch(hint);
expect(errors).toEqual([]);
});
+ /* #75: a magnet's range stops where the cup of a key put in from above starts. At 5 mm
+ the plate builds with Download on, 5.1 is refused and names the keys, and the same
+ keys put in from beneath take their recess's 6.1 again. */
+ test('a corner boss\'s pocket is held out of the cup a key drops into from above', async ({ page }) => {
+ const at = '#pi=36.13&w=144.52&d=72.26&mm=custom&ml=0&mr=0&mf=0&mb=0&bw=88.26&bd=400&cn=bowtie' +
+ '&km=wall&ki=top&bm=bosses&mg=1';
+ const errors = await openAt(page, `${at}&md=5`);
+ const max = () => page.evaluate(() => document.getElementById('magnetD').max);
+ expect(await max()).toBe('5');
+ expect(await shown(page, 'errMagnet')).toBe(false);
+ expect(await text(page, 'pieceTail')).toMatch(/ready/);
+ expect(await exportOff(page)).toBe(false);
+ await H.setField(page, 'magnetD', '5.1');
+ expect(await text(page, 'errMagnet'))
+ .toMatch(/Magnet Ø must be 5 mm or less .* the housings the bowtie keys drop into from above; put in from beneath, the bowtie keys leave more room\./);
+ expect(await text(page, 'pieceTail')).toMatch(/not building/);
+ expect(await exportOff(page)).toBe(true);
+ await page.selectOption('#keyInsert', 'bottom');
+ await page.waitForFunction(() => /ready/.test(document.getElementById('pieceTail').textContent),
+ null, { timeout: 30000 });
+ expect(await max()).toBe('6.1');
+ expect(await shown(page, 'errMagnet')).toBe(false);
+ expect(await exportOff(page)).toBe(false);
+ expect(errors).toEqual([]);
+ });
test('corner pockets under an extra floor are cut into it, and Checks says so', async ({ page }) => {
const errors = await openAt(page, '#cn=none&bm=bosses&sc=1&bp=1');
expect(await text(page, 'warnings')).toMatch(