The geometry of cabinets on walls that are not at right angles or are curved: arc walls from radius, sweep or three points, cabinets stepped round an arc, and the corner cabinet at any angle (CabinetCraft add-on).
How the precision is kept
Each result is a closed-form expression in 64-bit numbers, and nearly every value returned is then rounded to 6 decimals of the job's unit by r6. Cabinets along an arc are spaced by a search on the angle that halves the interval 60 times. Sagittas are formed by subtracting two nearly equal numbers when the cabinet is narrow against the radius.
Open item. In arcFromThree, subtract the first point from the other two before forming the squares, so a small arc far from the origin keeps its digits.
Calculations under this policy
16 calculations in 1 source file. 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 end point of a wall from its start, length and angle.
(x + L·cos a, y + L·sin a), rounded to 6 decimals
The angle goes from degrees to radians as d × π ÷ 180. The point is rounded to 6 decimals of the job's unit, so a chain of typed walls carries that rounding at every corner.
How far the middle of a straight back of width w stands off a circle of radius r.
s = r − √(r² − w² ÷ 4)
For a narrow cabinet on a large radius the two terms are nearly equal and the subtraction loses digits; the form w² ÷ (4·(r + √(r² − w² ÷ 4))) would not. The square root's argument is kept at 0 or above.
addons/cabinetcraft/lib/angled.js
line 44
CadCraft 0.8.2
export function sagitta(r, w) { return r - Math.sqrt(Math.max(0, r * r - (w * w) / 4)); }
Arc through three points
64-bit
The radius, center and signed sweep of the arc picked as start, middle and end.
The circumcenter formula uses the squares of the absolute coordinates, so for an arc far from the origin compared with its radius the sums cancel and digits are lost. The collinear test |d| < 1e-9 is absolute on a value with the unit of length squared. Radius, sweep and center are rounded to 6 decimals.
addons/cabinetcraft/lib/angled.js
lines 46 to 65
CadCraft 0.8.2
export function arcFromThree(p0, pm, p1) {
const [ax, ay] = p0, [bx, by] = pm, [cx, cy] = p1;
const d = 2 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if (Math.abs(d) < 1e-9) return null;
const ux = ((ax * ax + ay * ay) * (by - cy) + (bx * bx + by * by) * (cy - ay) + (cx * cx + cy * cy) * (ay - by)) / d;
const uy = ((ax * ax + ay * ay) * (cx - bx) + (bx * bx + by * by) * (ax - cx) + (cx * cx + cy * cy) * (bx - ax)) / d;
const r = Math.hypot(ax - ux, ay - uy);
// the arc runs p0 → pm → p1; its sweep is the angle it covers about the centre (< 180 when the middle point is on the minor arc)
const ang = (x, y) => Math.atan2(y - uy, x - ux);
let s0 = ang(ax, ay), sm = ang(bx, by), s1 = ang(cx, cy);
const norm = (v) => { while (v < 0) v += 2 * Math.PI; while (v >= 2 * Math.PI) v -= 2 * Math.PI; return v; };
// counter-clockwise about the centre from p0 to p1 passing pm?
const ccw = norm(sm - s0) < norm(s1 - s0);
const total = ccw ? norm(s1 - s0) : norm(s0 - s1);
const sweep = deg(total);
// is the middle point to the right of p0→p1 (a bulge away from a room on the left)?
const cross = (cx - ax) * (by - ay) - (cy - ay) * (bx - ax);
const outward = cross < 0;
return { r: r6(r), sweep: r6(outward ? sweep : -sweep), cx: r6(ux), cy: r6(uy), sagMid: r6(Math.abs(cross) / Math.hypot(cx - ax, cy - ay)) };
}
Arc from two ends and a sweep
64-bit
The radius, center, chord, arc length and sagitta of an arc wall.
r = chord ÷ (2·sin(θ ÷ 2)); center = middle ± normal × r·cos(θ ÷ 2); arc length = r·θ; sagitta = r·(1 − cos(θ ÷ 2))
Sweeps outside 0.01 to 179.99 degrees are refused, which keeps the sine away from 0. The sagitta is r·(1 − cos), which subtracts nearly equal numbers for a small sweep. Radius, center, chord, arc length and sagitta are rounded to 6 decimals; the two end angles are not.
addons/cabinetcraft/lib/angled.js
lines 67 to 86
CadCraft 0.8.2
export function arcGeom(a, b, sweepDeg) {
const s = Number(sweepDeg);
if (!Number.isFinite(s) || Math.abs(s) < 0.01 || Math.abs(s) > 179.99) return null;
const dx = b[0] - a[0], dy = b[1] - a[1], chord = Math.hypot(dx, dy);
if (chord < 1e-9) return null;
const u = { x: dx / chord, y: dy / chord }, nl = { x: -u.y, y: u.x }; // nl: the room side (left)
const half = Math.abs(rad(s)) / 2;
const r = chord / (2 * Math.sin(half));
const h = r * Math.cos(half);
const bay = s > 0;
const mx = (a[0] + b[0]) / 2, my = (a[1] + b[1]) / 2;
const sign = bay ? 1 : -1; // a bay's centre is on the room side of the chord, a bow's on the far side
const cx = mx + nl.x * h * sign, cy = my + nl.y * h * sign;
const a0 = Math.atan2(a[1] - cy, a[0] - cx), a1 = Math.atan2(b[1] - cy, b[0] - cx);
const rr = Math.abs(rad(s));
return {
r: r6(r), cx: r6(cx), cy: r6(cy), chord: r6(chord), arcLen: r6(r * rr), sag: r6(r * (1 - Math.cos(half))), bay, sweep: s, sigma: bay ? 1 : -1, a0, a1,
t0: rot(u, -s / 2), t1: rot(u, s / 2), u, n: nl,
};
}
Points along an arc wall
64-bit
An arc wall as n straight pieces.
φᵢ = a₀ ± |θ|·i ÷ n; point = center + r·(cos φ, sin φ), rounded to 6 decimals
The first and last points are replaced by the wall's own end points. The points between are computed from the rounded radius and center and rounded again.
addons/cabinetcraft/lib/angled.js
lines 88 to 98
CadCraft 0.8.2
export function arcPoints(a, b, sweepDeg, n = 16) {
const g = arcGeom(a, b, sweepDeg);
if (!g) return [a.slice(), b.slice()];
const out = [];
for (let i = 0; i <= n; i++) {
const phi = g.a0 + g.sigma * (Math.abs(rad(g.sweep)) * i) / n;
out.push([r6(g.cx + g.r * Math.cos(phi)), r6(g.cy + g.r * Math.sin(phi))]);
}
out[0] = [a[0], a[1]]; out[n] = [b[0], b[1]];
return out;
}
Back line and half angle of each cabinet on an arc
64-bit
The radius of each cabinet's back line and the half angle it takes up.
bay: H = √(R² − w² ÷ 4); bow or curved front: H = R; half angle = atan2(w ÷ 2, H), or w ÷ (2R) for a curved front; front radius = H − d
R is the arc's radius already rounded to 6 decimals. A cabinet is refused when its width is not below 2R − 1e-9, or (in a bay) when its front radius is 1 or less in the job's unit.
addons/cabinetcraft/lib/angled.js
lines 113 to 118
CadCraft 0.8.2
const Hb = items.map((it) => (it.curved ? R : g.bay ? Math.sqrt(Math.max(0, R * R - (it.w * it.w) / 4)) : R));
for (let i = 0; i < items.length; i++) if (!(items[i].w < 2 * R - 1e-9)) out.invalid.push(i);
const half = (it, r) => (it.curved ? it.w / (2 * R) : Math.atan2(it.w / 2, r));
const off = items.map((it, i) => half(it, Hb[i]));
const front = items.map((it, i) => Hb[i] - it.d);
if (g.bay) for (let i = 0; i < items.length; i++) if (front[i] <= 1) out.invalid.push(i);
Spacing cabinets round an arc
64-bit
The angle at which each next cabinet stands so that it just clears the one before.
bisection on the angle step in 0…π ÷ 2, 60 halvings; clash when the footprints penetrate more than 1e-7
Sixty halvings of a quarter turn leave an interval of about 1e-18 rad, below what a 64-bit angle can resolve, so the search stops at the resolution of the numbers. The upper end of the interval is kept, the side where the cabinets do not clash. The clash limit of 1e-7 is absolute in the job's unit.
addons/cabinetcraft/lib/angled.js
lines 143 to 152
CadCraft 0.8.2
let phi = g.a0 + sg * (from / R) + sg * off[0];
const phis = [phi];
let prev = place(items[0], 0, phi);
for (let i = 1; i < items.length; i++) {
let lo = 0, hi = Math.PI / 2;
for (let k = 0; k < 60; k++) { const mid = (lo + hi) / 2; if (clash(prev, place(items[i], i, phi + sg * mid))) lo = mid; else hi = mid; }
phi += sg * hi;
phis.push(phi);
prev = place(items[i], i, phi);
}
Gaps and wedges between stepped cabinets
64-bit
The largest gap between each cabinet's back and the curve, and the wedge left between neighbors.
bay gap = R − H; bow gap = √(R² + w² ÷ 4) − R; wedge back and front = distance between neighboring corners
Both gap formulas subtract two nearly equal numbers when the cabinet is narrow against the radius. The wedge widths are distances between corner points that were already rounded to 6 decimals, and are rounded again.
The tangent of τ ÷ 2 grows without limit as τ nears 180 degrees; the cabinet is marked not valid unless the open leg ℓ is above 1 (in the job's unit) and τ is between 0 and 180. Points are rounded to 6 decimals and the area is the shoelace sum of those rounded points.
addons/cabinetcraft/lib/angled.js
lines 189 to 215
CadCraft 0.8.2
/**
* The diagonal-front corner cabinet at a corner of turn τ (any convex τ from 1° to 179°; not 90: that one is corner.js). Frame: x along the first wall,
* y out from it, the wall corner at (w, 0), the next wall leaving at turn τ. Arms of length w and depth d. The footprint is split into two mitred arm
* boards (the mitre runs from the wall corner to the inside corner Q) and an isosceles triangle that closes the front.
* A = [(0,0), (w,0), Q, (0,d)] B (in its own frame, x along the next wall from the wall corner, y out) = [(0,0), (w,0), (w,d), (d·tan(τ/2), d)]
* triangle legs ℓ = w − d·tan(τ/2), apex (at Q) 180° − τ, front length L = 2ℓ·cos(τ/2)
*/
export function diagAt(tau, w, d) {
const t = rad(tau), th = Math.tan(t / 2);
const ell = w - d * th;
const Q = [r6(w - d * th), d];
const uB = { x: Math.cos(t), y: Math.sin(t) }, nB = { x: -Math.sin(t), y: Math.cos(t) };
const toA = (s, y) => [r6(w + uB.x * s + nB.x * y), r6(uB.y * s + nB.y * y)]; // a point in B's own frame → A's frame
const P1 = [w, 0], P2 = toA(w, 0), FB = toA(w, d), FA = [0, d];
const mitre = Math.hypot(Q[0] - w, Q[1]);
const L = 2 * ell * Math.cos(t / 2);
const gamma = 180 - tau;
return {
tau, w, d, ell: r6(ell), Q, P1, P2, FA, FB, front: r6(L), apex: r6(gamma), mitre: r6(mitre), mitreAngle: r6(90 - tau / 2),
valid: ell > 1 && tau > 0 && tau < 180,
A: [[0, 0], P1, Q, FA], B: [P1, P2, FB, Q],
outline: [[0, 0], P1, P2, FB, FA],
tri: [FA, Q, FB],
area: r6(polyArea([[0, 0], P1, P2, FB, FA])),
uB, nB,
};
}
Area of a footprint
64-bit
The area of a polygon.
A = |Σ (xᵢ·yᵢ₊₁ − xᵢ₊₁·yᵢ)| ÷ 2
The shoelace sum in point order. The footprints it is used on are in the cabinet's own frame, with a corner at the origin.
Chord, sagitta and circumcircle formulas; rotation by sine and cosine; bisection on the polar angle with a convex overlap test (footprint.js).
Number types
64-bit numbers; points are arrays [x, y] or {x, y} objects.
Tolerances
r6 rounds to 1e-6 of the job's unit. Three points are collinear when twice their signed area is below 1e-9 in size. An arc must sweep between 0.01 and 179.99 degrees and its chord be at least 1e-9. Cabinets clash when they penetrate more than 1e-7. Validity limits of 1 (front radius above 1, open leg above 1) are in the job's unit: 1 mm or 1 in.
Limits
The unit tangents of an arc are rounded to 6 decimals, about 1e-6 rad in direction. The three-point circle uses the squares of the absolute coordinates.
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.