Resources Numerical accuracy Calculation policy

Cabinets on angled and curved walls

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.

addons/cabinetcraft/lib/angled.js CadCraft 0.8.2 64-bit 16 calculations Reviewed

Tests None found by the review

Turning a direction

64-bit

A direction turned by an angle in degrees.

(x·cos a − y·sin a, x·sin a + y·cos a), each rounded to 6 decimals

The result is rounded to 6 decimals, so a unit direction comes back with a direction error of up to about 1e-6 rad and a length that is not exactly 1.

  • addons/cabinetcraft/lib/angled.js
  • line 32
  • CadCraft 0.8.2
export const rot = (v, d) => { const a = rad(d), c = Math.cos(a), s = Math.sin(a); return { x: r6(v.x * c - v.y * s), y: r6(v.x * s + v.y * c) }; };

Turn between two walls

64-bit

The signed angle from one wall direction to the next, in degrees.

atan2(u₀ × u₁, u₀ · u₁) × 180 ÷ π

atan2 of the cross and dot products keeps its digits for all angles, including near 0 and 180 degrees. Not rounded.

  • addons/cabinetcraft/lib/angled.js
  • line 33
  • CadCraft 0.8.2
export const turnBetween = (u0, u1) => deg(Math.atan2(u0.x * u1.y - u0.y * u1.x, u0.x * u1.x + u0.y * u1.y));

End of a wall typed as length and angle

64-bit

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.

  • addons/cabinetcraft/lib/angled.js
  • lines 35 to 38
  • CadCraft 0.8.2
export function edgeEnd(from, length, angleDeg) {
  const a = rad(angleDeg);
  return [r6(from[0] + length * Math.cos(a)), r6(from[1] + length * Math.sin(a))];
}

End of an arc typed as radius and sweep

64-bit

The end point of an arc wall from its radius, sweep and the heading of its chord.

chord = 2·R·sin(|sweep| ÷ 2); end = start + chord at the heading

One sine for the chord, then the same step as a straight wall, rounded to 6 decimals.

  • addons/cabinetcraft/lib/angled.js
  • lines 39 to 43
  • CadCraft 0.8.2
export function arcEnd(from, headingDeg, radius, sweepDeg) {
  const half = Math.abs(rad(sweepDeg)) / 2;
  const chord = 2 * radius * Math.sin(half);
  return edgeEnd(from, chord, headingDeg);
}

Sagitta

64-bit

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.

d = 2·(aₓ(bᵧ − cᵧ) + bₓ(cᵧ − aᵧ) + cₓ(aᵧ − bᵧ)); uₓ = Σ |p|²·(Δy) ÷ d; uᵧ = Σ |p|²·(Δx) ÷ d; r = |a − u|

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);

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.

  • addons/cabinetcraft/lib/angled.js
  • lines 156 to 169
  • CadCraft 0.8.2
  const gap = it.curved ? 0 : g.bay ? R - Hb[i] : Math.sqrt(R * R + (it.w * it.w) / 4) - R;
  const Pr = (s, y) => { const q = P(s, y); return { x: r6(q.x), y: r6(q.y) }; };
  cells.push({ i, frame: { o, u: { x: r6(u.x), y: r6(u.y) }, n: { x: r6(n.x), y: r6(n.y) }, ang: Math.atan2(u.y, u.x) }, phi: p, back: Hb[i], gap: r6(gap), gapAt: g.bay ? 'middle' : 'corners',
    corners: { bl: Pr(0, 0), br: Pr(it.w, 0), fl: Pr(0, it.d), fr: Pr(it.w, it.d) } });
  if (it.curved) {   // the ring sector in slices (world coordinates), and its radii
    const psi = it.w / R, rF = g.bay ? R - it.d : R + it.d;
    cells[cells.length - 1].curved = { rBack: r6(R), rFront: r6(rF), psi: r6(deg(psi)), arcLen: it.w, slices: place(it, i, p).pieces.map((pc) => pc.map((q) => [r6(q[0]), r6(q[1])])) };
  }
});
for (let i = 1; i < cells.length; i++) {
  const A = cells[i - 1].corners, B = cells[i].corners;
  const dist = (p, q) => Math.hypot(p.x - q.x, p.y - q.y);
  out.wedges.push({ i, back: r6(dist(A.br, B.bl)), front: r6(dist(A.fr, B.fl)), poly: [A.br, B.bl, B.fl, A.fr].map((q) => [q.x, q.y]),
    angle: r6(Math.abs(deg(phis[i] - phis[i - 1]))) });

Length used along the arc

64-bit

How much of the arc the cabinets take, and by how much the last one passes the end.

used = (end angle − start angle) × R; overflow = max(0, end angle − limit) × R

Angles are differenced first and multiplied by the radius once. Both results are rounded to 6 decimals.

  • addons/cabinetcraft/lib/angled.js
  • lines 172 to 175
  • CadCraft 0.8.2
const endPhi = phis[last] + sg * off[last];
const limit = g.a0 + sg * ((to === null ? g.arcLen : to) / R);
out.overflow = r6(Math.max(0, sg * (endPhi - limit)) * R);   // length along the arc by which the last cabinet passes the end
out.used = r6(sg * (endPhi - (g.a0 + sg * from / R)) * R);

Stop short of an angled corner

64-bit

How far a run of cabinets must stop before a convex corner.

τ ≤ 90°: depth × sin τ; sharper: depth

One sine and one product, rounded to 6 decimals.

  • addons/cabinetcraft/lib/angled.js
  • lines 179 to 182
  • CadCraft 0.8.2
export function cornerBlock(tau, depth) {
  if (tau <= 0) return 0;
  return r6(tau <= 90 ? depth * Math.sin(rad(tau)) : depth);
}

Void at an angled corner

64-bit

The right-triangle void left where a run stops short of an angled corner.

leg₁ = d·sin τ; leg₂ = d·cos τ; area = leg₁·leg₂ ÷ 2

The area is formed from the unrounded legs and then rounded to 6 decimals, as are the legs.

  • addons/cabinetcraft/lib/angled.js
  • lines 183 to 187
  • CadCraft 0.8.2
export function wedgeCorner(tau, depth) {
  if (!(tau > 0 && tau <= 90)) return null;
  const leg1 = depth * Math.sin(rad(tau)), leg2 = depth * Math.cos(rad(tau));
  return { leg1: r6(leg1), leg2: r6(leg2), hyp: r6(depth), area: r6((leg1 * leg2) / 2) };
}

Diagonal corner cabinet at any angle

64-bit

The outline, miter, front length and area of a diagonal-front corner cabinet at a corner of turn τ.

ℓ = w − d·tan(τ ÷ 2); front = 2·ℓ·cos(τ ÷ 2); apex = 180° − τ; miter angle = 90° − τ ÷ 2; Q = (w − d·tan(τ ÷ 2), d)

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.

  • addons/cabinetcraft/lib/angled.js
  • line 216
  • CadCraft 0.8.2
export function polyArea(pts) { return Math.abs(pts.reduce((s, p, i) => { const q = pts[(i + 1) % pts.length]; return s + (p[0] * q[1] - q[0] * p[1]); }, 0)) / 2; }

In detail

Method
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.

Every policy that takes this approach