Places every cabinet, filler and end panel along its wall or island, works out how runs meet at corners, and sizes the countertops.
How the precision is kept
Positions along a run are running sums of cabinet widths in 64-bit numbers; each stored position is rounded to 6 decimals of the job unit (r6, core.js). Fits are decided with an absolute tolerance of 0.01 job units. On curved walls a filler width and a corner shift are found by bisection with a fixed number of halvings, and the room a curve takes at a corner uses the first term s² ÷ 2r. Footprint overlap is a separating-axis test with an absolute 1e-6 threshold.
Open item. Scale the 0.01 layout tolerance to the job unit, so an inch job does not use a tolerance 25.4 times larger than a millimeter job.
Calculations under this policy
19 calculations in 2 source files. The code is read from the released app when this page loads. It is the calculation people run, not a copy kept for this page.
The origin and axes of each island's front row and back row.
u = (cos a, sin a), a = rot × π ÷ 180; back row origin = o + u × len
The cosine and sine are rounded to 6 decimals. cleanIslands in the same file only allows rot of 0, 90, 180 or 270, so the rounded axes are exactly 0 or ±1. The back row's origin is rounded to 6 decimals.
addons/cabinetcraft/lib/project.js
lines 171 to 189
CadCraft 0.8.2
/** The islands as runs of their own: the front row faces the island's left, a back row (if it has items) faces the other way from the far end. */
export function islandRuns(p) {
const out = [];
for (const isl of p.islands || []) {
const a = (isl.rot * Math.PI) / 180;
const u = { x: r6(Math.cos(a)), y: r6(Math.sin(a)) }, n = { x: r6(-u.y), y: r6(u.x) };
const o = { x: isl.x, y: isl.y };
const mk = (id, items, frame, attach, ep) => ({
run: { id, name: isl.name, wall: null, island: isl.id, from: 0, to: isl.len, autoFiller: isl.autoFiller, endPanels: ep, base: items, upper: [], attach, seat: isl.seat, kind: isl.kind },
wall: null, frame, len: isl.len, rows: {}, closedStart: attach.start, closedEnd: attach.end, island: isl,
});
out.push(mk(isl.id, isl.base, { o, u, n, ang: a }, isl.attach, isl.endPanels));
if (isl.back.length) {
const o2 = { x: r6(o.x + u.x * isl.len), y: r6(o.y + u.y * isl.len) };
out.push(mk(isl.id + 'b', isl.back, { o: o2, u: { x: -u.x, y: -u.y }, n: { x: -n.x, y: -n.y }, ang: a + Math.PI }, { start: isl.attach.end, end: isl.attach.start }, { start: isl.endPanels.end, end: isl.endPanels.start }));
}
}
return out;
}
Layout tolerance and largest automatic filler
64-bit
The two limits the layout compares against.
tol = 0.01; maxFiller = conv(75, unit)
tol is 0.01 in job units whatever the unit. conv (core.js) rounds 75 ÷ 25.4 to the nearest 1/16, so in an inch job the largest automatic filler is 2.9375 in, which is 74.6125 mm.
addons/cabinetcraft/lib/project.js
lines 210 to 211
CadCraft 0.8.2
const tol = 0.01;
const maxFiller = conv(75, unit);
End-face swing of a cabinet on a bay
64-bit
How far the end face of a straight cabinet on a curved bay swings toward the neighboring run.
tilt = d × sin(atan2(w ÷ 2, √(r² − w² ÷ 4)))
The expression equals d × w ÷ (2r); it is written with a square root, atan2 and sin, each of which rounds once. The square root's argument is kept at 0 or above, and a cabinet as wide as the arc's diameter returns 0.
How far short of a room corner a run stops so it clears the run on the other wall.
stop = flat + tilt(A) + tilt(B) + flat² ÷ (2r); flat = other run's depth at 90°
On an arc wall the bend term q² ÷ 2r is the first term of the distance a circle leaves its tangent, not the exact value r − √(r² − q²). The sum is not rounded. A wedge is recorded when the corner is not square or the stop exceeds the depth by more than 1e-6.
addons/cabinetcraft/lib/project.js
lines 291 to 297
CadCraft 0.8.2
const later = ib > ia;
const dOther = later ? a.depth : b.depth;
const flat = square ? dOther : cornerBlock(tau, dOther);
const bend = (L, q) => (L.wallInfo.arc && !L.island ? (q * q) / (2 * L.wallInfo.arc.r) : 0); // the curve leaves the tangent line by s^2 / 2r over a stretch s
const stop = flat + arcTilt(A, row, 'end') + arcTilt(B, row, 'start') + bend(A, flat);
if (later) b.blocked.start = Math.max(b.blocked.start, stop); else a.blocked.end = Math.max(a.blocked.end, stop);
if (!square || stop > dOther + 1e-6) {
Wedge filler triangle at an angled corner
64-bit
The three corners of the triangular void left where two runs meet at an angle that is not 90 degrees.
tri = [corner, corner ± u × leg1, corner + n × depth]
The legs and area come from wedgeCorner in angled.js. The triangle's points are the room corner plus a wall direction times a length; the directions are the wall frames' 6-decimal unit vectors. A wedge with a leg of 1 job unit or less is not recorded.
The width of the filler that closes the gap between the last cabinet and the end of the span.
rest = end − start − used; filler = rest when tol < rest ≤ maxFiller
On a straight wall the filler is the remaining length, rounded to 6 decimals. On a curved wall, when that width does not fit, 30 halvings find the widest one that does; the result is the lower bound of the last interval. A width under 0.01 becomes no filler.
addons/cabinetcraft/lib/project.js
lines 336 to 351
CadCraft 0.8.2
let used = sumW(L, row, ordered) + (tail ? R.frontT : 0);
let rest = rw.end - rw.start - used;
if (cells.length && L.run.autoFiller && rest > tol && rest <= maxFiller) {
let wf = rest;
if (L.wallInfo && L.wallInfo.arc && !L.island) { // on a curve a filler also adds the wedge on either side of it: take the widest one that still fits the span
const span = rw.end - rw.start - (tail ? R.frontT : 0);
const fits = (v) => sumW(L, row, [...ordered, { kind: 'filler', w: v, depth: 0 }]) <= span + tol;
if (!fits(wf)) { let lo = 0, hi = wf; for (let k = 0; k < 30; k++) { const mid = (lo + hi) / 2; if (fits(mid)) lo = mid; else hi = mid; } wf = lo; }
}
if (wf < tol) wf = 0;
const f = wf > 0 ? { kind: 'filler', w: r6(wf), auto: true, row, height: fillerHeight(ordered, R), depth: 0 } : null;
if (!f) { /* no room for a filler on this curve */ } else {
if (rw.cornerEnd) ordered.splice(ordered.length - 1, 0, f); else ordered.push(f); // beside a corner cabinet the filler goes before it
used += f.w; rest = r6(rest - f.w);
}
}
Position of each cell along its run
64-bit
The distance from the start of the wall to each cabinet, filler and end panel.
xᵢ₊₁ = xᵢ + wᵢ; stored xᵢ rounded to 6 decimals
The running sum x is kept unrounded and each stored position is a rounded copy, so the rounding does not build up along the run. A corner cabinet at the end of a run is set at end − width.
addons/cabinetcraft/lib/project.js
lines 353 to 356
CadCraft 0.8.2
let x = rw.start;
for (const c of ordered) {
if (c === rw.cornerEnd) x = Math.max(x, rw.end - c.w); // the corner cabinet sits IN the corner, whatever is left over is before it
c.x = r6(x); x += c.w;
First cabinet on a curve moved clear of the previous run
64-bit
On a curved wall that starts at a room corner, how far the first cabinet is moved along so it does not overlap the previous run's last cabinet.
bisection on [0, 600 mm], 40 halvings; clash when overlap > 1e-6
The search runs only when the cabinet clashes at 0 and is clear at conv(600), which is 23.625 in an inch job. After 40 halvings the interval is 600 ÷ 2⁴⁰ wide and the upper, clear end is taken. The stored shift is rounded to 6 decimals.
addons/cabinetcraft/lib/project.js
lines 385 to 389
CadCraft 0.8.2
if (clashes(0)) {
let lo = 0, hi = conv(600, unit);
if (!clashes(hi)) { for (let k = 0; k < 40; k++) { const mid = (lo + hi) / 2; if (clashes(mid)) lo = mid; else hi = mid; } shift = hi; }
}
if (shift > 0) { st = stepArc(g, items, { from: rw.start + shift, to: rw.end }); rw.arcShift = r6(shift); }
Straight countertop length and width
64-bit
The length and width of a countertop over a straight run of base cabinets.
length = x₁ − x₀ + overhang at each open end; width = depth + front overhang + seat
Differences and sums of stored positions and the counter settings, rounded to 6 decimals. The depth is the largest cabinet depth in the run.
The outline and area of a one-piece countertop that follows cabinets stepped along a curved wall.
area = |Σ (xᵢ yᵢ₊₁ − xᵢ₊₁ yᵢ)| ÷ 2
The outline's points are rounded to 6 decimals before the shoelace sum. The sum uses room coordinates, not coordinates relative to the piece, so far from the origin the products are large next to their difference. The reported length is the sum of the cabinet widths, not a length along the outline.
The outline and area of the countertop over a corner cabinet.
area = shoelace of the outline; length = width = √area
The diagonal form pushes the front out by f × √2 using Math.SQRT2. The outline is rounded to 6 decimals, then the area is a shoelace sum. The length and width stored for the piece are both √area, not measured sides.
addons/cabinetcraft/lib/project.js
lines 443 to 454
CadCraft 0.8.2
const d = depthOf ? depthOf(c) : c.cab.d, f = p.counter.front, w = c.cab.w, a = w - d;
let pts = c.turn === 'l' ? [[0, 0], [w, 0], [w, w], [a - f, w], [a - f, d + f], [0, d + f]] : [[0, 0], [w, 0], [w, w], [a - f * Math.SQRT2, w], [0, d + f * Math.SQRT2]];
if (c.turn === 'diag' && c.tau && Math.abs(c.tau - 90) >= 0.01) { // at another angle: the outline of diagAt with its front pushed out by the overhang
const dg = diagAt(c.tau, w, d), dx = dg.FB[0] - dg.FA[0], dy = dg.FB[1] - dg.FA[1], ln = Math.hypot(dx, dy) || 1, nx = dy / ln, ny = -dx / ln; // the front's normal away from the corner
const sgn = (nx * (dg.Q[0] - dg.FA[0]) + ny * (dg.Q[1] - dg.FA[1])) > 0 ? -1 : 1;
pts = [[0, 0], dg.P1, dg.P2, [dg.FB[0] + sgn * nx * f, dg.FB[1] + sgn * ny * f], [dg.FA[0] + sgn * nx * f, dg.FA[1] + sgn * ny * f]];
}
if (c.corner === 'start') pts = pts.map(([x, y]) => [w - x, y]).reverse();
pts = pts.map(([x, y]) => [r6(c.x + x), r6(y)]);
const area = Math.abs(pts.reduce((q, pt, i) => { const n = pts[(i + 1) % pts.length]; return q + pt[0] * n[1] - n[0] * pt[1]; }, 0)) / 2;
const xs = pts.map((q) => q[0]), ys = pts.map((q) => q[1]), side = Math.sqrt(area);
cs.push({ run: L.run.id, wall: L.wall, corner: c.turn, cab: c.no, poly: pts, x0: Math.min(...xs), x1: Math.max(...xs), y0: Math.min(...ys), y1: Math.max(...ys), depth: d, cabs: 1, ids: [c.no], ends: { start: 'wall', end: 'wall' }, length: r6(side), width: r6(side), area: r6(area) });
Extent of a cell along its wall
64-bit
The stretch of wall each cell covers, used to test it against doors and windows.
curved wall: min and max of (corner − a)·u; straight: [x, x + w]
On a curved wall the four corners are projected on the chord with the wall's 6-decimal direction, and the result is rounded to 6 decimals.
The polygon each cabinet and end panel covers on the floor, its bounding box and its height range.
Rectangles are built in the cell's frame and moved to room coordinates by toWorld, so their corners carry its 6-decimal rounding. The bounding box is the minimum and maximum of those corners. z₁ = elevation + height is not rounded.
addons/cabinetcraft/lib/footprint.js
lines 18 to 45
CadCraft 0.8.2
export function footprints(build) {
const { layout, R, F, project } = build;
const out = [];
const byNo = new Map(build.cells.map((c) => [c.no, c]));
for (const L of layout.runs) for (const row of ['base', 'upper']) for (const c of L.rows[row].cells) {
if (c.kind === 'gap' || c.kind === 'filler') continue;
let local;
let z0 = 0, z1 = 0;
if (c.kind === 'endpanel') { const sx = c.pf ? 0 : c.x; local = [[[sx, 0], [sx + c.w, 0], [sx + c.w, c.depth], [sx, c.depth]]]; z1 = c.height; }
else {
const b = byNo.get(c.no);
const sx = c.pf ? 0 : c.x;
if (c.curved) local = null;
else if (b && b.corner) local = b.corner.footprint.map((poly) => poly.map(([x, y]) => [sx + x, y]));
else {
const depth = c.cab.d; // the carcass (and frame): doors stand a few mm proud at a corner, which is what a filler is for
local = [[[sx, 0], [sx + c.w, 0], [sx + c.w, depth], [sx, depth]]];
}
z0 = c.cab.elev || 0; z1 = z0 + c.cab.h;
}
const fr = c.pf || L.frame;
const pieces = local ? local.map((poly) => poly.map(([s, o]) => { const p = toWorld(fr, s, o); return [p.x, p.y]; })) : c.curved.slices; // a curved-front cabinet: its ring sector in convex slices
const xs = pieces.flat().map((p) => p[0]), ys = pieces.flat().map((p) => p[1]);
out.push({ no: c.no, run: L.run.id, island: L.island ? L.island.id : null, row, kind: c.kind, cell: c, pieces, bbox: [Math.min(...xs), Math.min(...ys), Math.max(...xs), Math.max(...ys)], z0, z1 });
}
void F; void project;
return out;
}
How far two footprints overlap
64-bit
The penetration depth of two convex polygons, 0 when they are apart or only touch.
for each edge normal n: overlap = min(a₁, b₁) − max(a₀, b₀) of the projections; result = smallest overlap
Separating-axis test with each normal divided by its length from Math.hypot. An overlap of 1e-6 or less on any axis counts as not overlapping, an absolute value in job units. Edges shorter than 1e-9 are skipped.
addons/cabinetcraft/lib/footprint.js
lines 47 to 63
CadCraft 0.8.2
/** Penetration depth of two convex polygons (separating axis test); 0 when they only touch or are apart. */
export function overlap(A, B) {
let best = Infinity;
for (const poly of [A, B]) {
for (let i = 0; i < poly.length; i++) {
const p = poly[i], q = poly[(i + 1) % poly.length];
const nx = q[1] - p[1], ny = p[0] - q[0], len = Math.hypot(nx, ny);
if (len < 1e-9) continue;
const proj = (P) => { let lo = Infinity, hi = -Infinity; for (const [x, y] of P) { const v = (x * nx + y * ny) / len; if (v < lo) lo = v; if (v > hi) hi = v; } return [lo, hi]; };
const [a0, a1] = proj(A), [b0, b1] = proj(B);
const o = Math.min(a1, b1) - Math.max(a0, b0);
if (o <= 1e-6) return 0;
if (o < best) best = o;
}
}
return Number.isFinite(best) ? best : 0;
}
Gap between two rectangles
64-bit
The distance between two axis-aligned rectangles.
gap = √(dx² + dy²), dx and dy the clear distances on each axis, 0 when they overlap on it
Two subtractions per axis and one Math.hypot; nothing is rounded.
The smallest distance between two footprints made of convex pieces, 0 when they touch or overlap.
The minimum of every corner-to-edge distance in both directions, which is the exact distance for polygons that do not overlap. Overlap is decided first with the 1e-6 threshold of overlap().
addons/cabinetcraft/lib/footprint.js
lines 70 to 79
CadCraft 0.8.2
/** Distance between two sets of convex polygons (0 when they touch or overlap). */
export function polyGap(setA, setB) {
const segDist = (p, a, b) => { const dx = b[0] - a[0], dy = b[1] - a[1], L2 = dx * dx + dy * dy; const t = L2 ? Math.max(0, Math.min(1, ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / L2)) : 0; return Math.hypot(p[0] - a[0] - t * dx, p[1] - a[1] - t * dy); };
let best = Infinity;
for (const A of setA) for (const B of setB) {
if (overlap(A, B) > 0) return 0;
for (const [P, Q] of [[A, B], [B, A]]) for (const p of P) for (let i = 0; i < Q.length; i++) best = Math.min(best, segDist(p, Q[i], Q[(i + 1) % Q.length]));
}
return best === Infinity ? 0 : best;
}
In detail
Method
Running-sum placement; corner stop = the other run's depth (or cornerBlock in angled.js for other angles) plus tilt and bend terms on arc walls; bisection for a filler on a curve (30 halvings) and for the first cabinet's shift at a corner (40 halvings of a 600 mm interval); shoelace sum for countertop areas; separating-axis test for convex footprints.
Number types
64-bit numbers; footprints are arrays of [x, y] pairs.
Tolerances
tol = 0.01 job units for reaching a wall end, adjacency and filler size; a corner is square when its turn is within 0.01 degrees of 90; largest automatic filler 75 mm (2.9375 in in an inch job); overlap counted above 1e-6; polygon edges shorter than 1e-9 skipped. All absolute.
Limits
The 0.01 tolerance is 0.01 mm in a millimeter job and 0.01 inch in an inch job. A corner countertop's length and width are both reported as the square root of its area.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.