Skip to content
Merged
103 changes: 98 additions & 5 deletions bins/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4340,6 +4340,7 @@ <h2 id="drawersTitle">Your drawers</h2>
* 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]]);
Expand Down Expand Up @@ -5100,10 +5101,13 @@ <h2 id="drawersTitle">Your drawers</h2>
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
Expand All @@ -5122,7 +5126,7 @@ <h2 id="drawersTitle">Your drawers</h2>
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);
Expand Down Expand Up @@ -5745,12 +5749,17 @@ <h2 id="drawersTitle">Your drawers</h2>
// 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) =>
Expand Down Expand Up @@ -5888,6 +5897,35 @@ <h2 id="drawersTitle">Your drawers</h2>
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]);
Expand Down Expand Up @@ -5976,6 +6014,56 @@ <h2 id="drawersTitle">Your drawers</h2>
// 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 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
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. 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, 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;
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,
Expand Down Expand Up @@ -6146,6 +6234,11 @@ <h2 id="drawersTitle">Your drawers</h2>
const pocket = movePolys(fastener, ccx + sx*off, ccy + sy*off);
const first = csgSubtract(boss, pocket);
boss = first.open || clipped && first.turned ? cutAgain(boss, pocket, first) : first;
if (boss.open) // a cap it is open on fanned again (refanCaps)
boss = refanCaps(boss, world, bossH, fastener, [all[n]], (prism) => {
const again = csgSubtract(prism, pocket);
return again.open || clipped && again.turned ? cutAgain(prism, pocket, again) : again;
});
}
return boss;
}));
Expand Down
49 changes: 47 additions & 2 deletions docs/ENGINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1021,6 +1024,48 @@ 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 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
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 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 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
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 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
Expand Down
Loading
Loading