Resources Numerical accuracy Calculation policy

Framing panels and cut lists

Lays out the studs, plates, headers, joists and rafters of a framing panel, lists the pieces to cut, and places them in the model as members.

How the precision is kept

Piece positions and lengths are closed-form sums and differences in 64-bit millimeters. Every piece is rounded to 0.1 mm when it is stored, the cut list totals are rounded to the millimeter, and the end points of members placed in the model are rounded to 0.1 mm after the panel's rotation. The stored values are therefore tenths of a millimeter, not full 64-bit results.

Open item. Keep piece lengths and placed member end points unrounded and round them only where they are printed.

Calculations under this policy

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

framing.js BimCraft 0.8.1 64-bit 12 calculations Reviewed

Tests tools/detailcraft-test/test-framing.mjs

A piece as stored

64-bit

One stud, plate, header, joist or rafter with its length and its place in the panel.

stored value = round(v × 10) ÷ 10

The length, position, width and height of every piece are rounded to 0.1 mm before they are stored. Everything downstream (marks, cut list, drawing, placed members) reads the rounded values.

  • framing.js
  • line 45
  • BimCraft 0.8.1
function addPiece(list, kind, size, length, x, y, w, h) { list.push({ id: kind + (list.length + 1), kind, size, length: r1(length), x: r1(x), y: r1(y), w: r1(w), h: r1(h) }); }

Stud length of a wall

64-bit

The length of a full-height stud: the wall height less the plates.

stud length = H − (top plates + 1) × plate thickness

One product and one subtraction on the dressed size from the table. The number of top plates is cut to a whole number with a bitwise | 0 and held between 1 and 3; it is a count, not a measurement.

  • framing.js
  • lines 52 to 56
  • BimCraft 0.8.1
const lgs = !!sz.steel, tp = lgs ? sz.t : sz.face;                        // a plate's (track's) thickness on the elevation
const nTop = lgs ? 1 : Math.max(1, Math.min(3, spec.topPlates | 0)), nBot = 1;
const L = spec.length, H = spec.height;
const plateStack = (nTop + nBot) * tp;
const studLen = H - plateStack, studY = nBot * tp;

Studs on the spacing grid

64-bit

Where the regular studs stand, left out where an opening is.

center c = spacing, 2 × spacing, …; stud from c − face ÷ 2 to c + face ÷ 2

The centers are reached by adding the spacing repeatedly, not by multiplying, so a spacing that binary cannot hold exactly adds its rounding up along the wall; the position is then rounded to 0.1 mm. Overlap with an opening band is tested with 1e-6 mm of slack.

  • framing.js
  • lines 74 to 79
  • BimCraft 0.8.1
const inBand = (x0, x1) => bands.some(([b0, b1]) => x1 > b0 + 1e-6 && x0 < b1 - 1e-6);
for (let c = spec.spacing; c < L - face; c += spec.spacing) {
  const x0 = c - face / 2;
  if (x0 < face + 1e-6 || x0 + face > L - face - 1e-6) continue;
  if (!inBand(x0, x0 + face)) addPiece(pieces, 'stud', spec.size, studLen, x0, studY, face, studLen);
}

Framing round an opening

64-bit

The jack studs, header, cripples and sill of one opening.

jack = sill + opening height − bottom plate; header = opening width + 2 × stud face; cripple above = H − top plates − header top

Sums and differences of the opening's sizes and the dressed sizes. A jack or cripple is made only when it is longer than 1 mm. A header that would pass the top plates is reported with 1e-6 mm of slack.

  • framing.js
  • lines 85 to 101
  • BimCraft 0.8.1
const headerBottom = o.sill + o.h;                                                                           // the rough opening is measured from the panel's underside (the subfloor)
const hdrDepth = hd ? hd.depth : face, headerTop = headerBottom + hdrDepth;
if (headerTop > H - nTop * tp + 1e-6) issues.push({ code: 'tall', at: o.x });
const jack = headerBottom - studY;                                                                         // the jacks stand on the bottom plate and carry the header
if (jack > 1) { addPiece(pieces, 'jack', spec.size, jack, b0 + face, studY, face, jack); addPiece(pieces, 'jack', spec.size, jack, b1 - 2 * face, studY, face, jack); }
// the header: between the kings, on the jacks
const hLen = o.w + 2 * face;
for (let p = 0; p < (hd ? Math.max(1, spec.headerPlies | 0) : 1); p++) addPiece(pieces, 'header', hd ? spec.headerSize : spec.size, hLen, b0 + face, headerBottom, hLen, hdrDepth);
// cripples over the header and under the sill, on the stud grid, between the kings
const above = H - nTop * tp - headerTop;
const below = o.sill - tp - studY;
for (const x0 of grid) {
  if (x0 < b0 + face - 1e-6 || x0 + face > b1 - face + 1e-6) continue;
  if (above > 1) addPiece(pieces, 'cripple', spec.size, above, x0, headerTop, face, above);
  if (o.sill > 0 && below > 1) addPiece(pieces, 'cripple', spec.size, below, x0, studY, face, below);
}
if (o.sill > 0) addPiece(pieces, 'sill', spec.size, o.w, b0 + 2 * face, o.sill - tp, o.w, tp);

Floor panel

64-bit

The joists, rim joists and blocking of a floor panel.

joist length = span − 2 × rim thickness; blocking = clear gap between neighboring joists

Joist positions are the spacing grid plus one at each edge; positions that agree after rounding to 0.1 mm are kept once. Blocking lengths are differences of the stored, already rounded joist positions.

  • framing.js
  • lines 106 to 124
  • BimCraft 0.8.1
export function floorPanel(specIn) {
  const spec = { ...FLOOR_DEFAULTS, ...specIn }, sz = sizeOf(spec.material, spec.size);
  const pieces = [], issues = [];
  if (!sz) return { spec, pieces, issues: [{ code: 'size', size: spec.size }], summary: summary([]) };
  const face = sz.face, W = spec.width, S = spec.span;
  const rimT = spec.rim ? face : 0;
  const joistLen = S - 2 * rimT;
  // joists across the width along the span (drawn as vertical bars in plan); the first and last are at the edges
  const xs = [0];
  for (let c = spec.spacing; c < W - face; c += spec.spacing) xs.push(c - face / 2);
  xs.push(W - face);
  for (const x of [...new Set(xs.map(r1))]) addPiece(pieces, 'joist', spec.size, joistLen, x, rimT, face, joistLen);
  if (spec.rim) { addPiece(pieces, 'rim', spec.size, W, 0, 0, W, face); addPiece(pieces, 'rim', spec.size, W, 0, S - face, W, face); }
  if (spec.blocking) {
    const ys = S / 2 - face / 2, js = pieces.filter((p) => p.kind === 'joist').sort((a, b) => a.x - b.x);
    for (let i = 1; i < js.length; i++) { const gap = js[i].x - (js[i - 1].x + face); if (gap > 1) addPiece(pieces, 'blocking', spec.size, gap, js[i - 1].x + face, ys, gap, face); }
  }
  return finish({ spec, pieces, issues, width: W, height: S });
}

Roof panel

64-bit

The rafters and ridge board of a gable roof panel, and its pitch.

slope = √(run² + rise²); rafter = slope + overhang × slope ÷ run; pitch = atan2(rise, run) × 180 ÷ π

The slope is Math.hypot(run, rise). The division by the run is not guarded in this function; the cleaned spec keeps the run at 600 mm or more. The rafter length runs along the slope from the ridge line to the end of the overhang: half the ridge board's thickness is not taken off. It is rounded to 0.1 mm.

  • framing.js
  • lines 126 to 146
  • BimCraft 0.8.1
export function roofPanel(specIn) {
  const spec = { ...ROOF_DEFAULTS, ...specIn }, sz = sizeOf(spec.material, spec.size), rg = sizeOf(spec.material, spec.ridge);
  const pieces = [], issues = [];
  if (!sz) return { spec, pieces, issues: [{ code: 'size', size: spec.size }], summary: summary([]) };
  const run = spec.run, rise = spec.rise, slope = Math.hypot(run, rise);
  const len = slope + spec.overhang * slope / run;                                                            // along the slope, eaves overhang measured horizontally
  const pairs = [0];
  for (let c = spec.spacing; c < spec.length - sz.face; c += spec.spacing) pairs.push(c - sz.face / 2);
  pairs.push(spec.length - sz.face);
  const pairAt = [...new Set(pairs.map(r1))];
  const nPairs = pairAt.length;
  const ridgeT = rg ? rg.face : sz.face;
  // gable elevation: both rafters of one pair, and the ridge board end
  const total = 2 * (run + spec.overhang);
  addPiece(pieces, 'rafter', spec.size, len, 0, 0, run + spec.overhang, rise * (run + spec.overhang) / run);
  addPiece(pieces, 'rafter', spec.size, len, run + spec.overhang, 0, run + spec.overhang, rise * (run + spec.overhang) / run);
  addPiece(pieces, 'ridge', spec.ridge, spec.length, run + spec.overhang - ridgeT / 2, rise * (run + spec.overhang) / run - (rg ? rg.depth : sz.depth) / 2, ridgeT, rg ? rg.depth : sz.depth);
  // one drawing of one pair; the counts are the pairs
  const counts = { rafter: 2 * nPairs, ridge: 1 };
  return finish({ spec, pieces, issues, width: total, height: rise * (run + spec.overhang) / run + 100, counts, pairAt, pitch: Math.atan2(rise, run) * 180 / Math.PI });
}

Piece count and total length

64-bit

How many pieces a panel has and their total length in meters.

meters = Σ piece lengths ÷ 1000, rounded to 0.01

The lengths added are the stored ones, already rounded to 0.1 mm, and the total is rounded to the centimeter. A roof weights each drawn piece by the number of pairs.

  • framing.js
  • lines 161 to 165
  • BimCraft 0.8.1
function summary(pieces, counts = null) {
  const n = counts ? Object.values(counts).reduce((a, b) => a + b, 0) : pieces.length;
  const metres = counts ? pieces.reduce((a, p) => a + p.length * (counts[p.kind] / pieces.filter((q) => q.kind === p.kind).length), 0) / 1000 : pieces.reduce((a, p) => a + p.length, 0) / 1000;
  return { pieces: n, metres: Math.round(metres * 100) / 100 };
}

Cut list

64-bit

One row per mark: size, length, count and total length.

total (m) = round(length × count) ÷ 1000

Pieces share a mark when kind, size and stored length are equal, so lengths that differ by less than 0.1 mm before rounding can share a row. The row total is rounded to the millimeter. A roof's count is the drawn pieces scaled to the number of pairs and rounded to a whole number.

  • framing.js
  • lines 167 to 179
  • BimCraft 0.8.1
/** One row per mark: { mark, kind, size, length, count, total (m) }. A roof counts its pairs. */
export function cutList(panel) {
  const rows = new Map();
  for (const pc of panel.pieces) {
    const k = pc.mark;
    if (!rows.has(k)) rows.set(k, { mark: pc.mark, kind: pc.kind, size: pc.size, length: pc.length, count: 0 });
    rows.get(k).count++;
  }
  const out = [...rows.values()];
  if (panel.counts) for (const r of out) { const same = panel.pieces.filter((p) => p.kind === r.kind).length; r.count = Math.round(r.count * panel.counts[r.kind] / same); }
  for (const r of out) r.total = Math.round(r.length * r.count) / 1000;
  return out.sort((a, b) => a.size.localeCompare(b.size) || b.length - a.length);
}

Panel placed in the model

64-bit

A point of the panel turned by the panel's angle and moved to its place in the model.

P = O + u·(cos a, sin a) + n·(−sin a, cos a) + (0, 0, z), each coordinate rounded to 0.1 mm

The rotation is done in 64-bit and each coordinate of the result is rounded to 0.1 mm. On a panel at an angle the two ends of a member are rounded separately, so the member's length in the model can differ from the piece's length by around a tenth of a millimeter.

  • framing.js
  • lines 281 to 283
  • BimCraft 0.8.1
const a = (place.angle || 0) * Math.PI / 180, ux = Math.cos(a), uy = Math.sin(a), nx = -Math.sin(a), ny = Math.cos(a);
const O = [place.x || 0, place.y || 0, place.z || 0];
const at = (u, n, z) => [Math.round((O[0] + ux * u + nx * n) * 10) / 10, Math.round((O[1] + uy * u + ny * n) * 10) / 10, Math.round((O[2] + z) * 10) / 10];

Timber profile and weight

64-bit

The profile a placed timber piece gets: its dressed size and a density for its weight.

b × t = dressed size, rounded to 0.1 mm; density = 500 kg/m³

One density, 500 kg/m³, is used for every timber piece; the code's comment places common softwoods between 420 and 530. A weight computed from it is good to that spread, not to the digits shown.

  • framing.js
  • lines 293 to 294
  • BimCraft 0.8.1
const name = 'TIM ' + r1(across) + 'x' + r1(up);
if (!profiles.has(name)) profiles.set(name, { name, kind: 'FLAT', b: r1(across), t: r1(up), density: TIMBER_DENSITY });

Scale of the shop sheet

64-bit

The scale the panel is drawn at on its sheet, and the scale printed in the title block.

k = min(drawing width ÷ panel width, drawing height ÷ panel height); printed scale = 1 : round(1 ÷ k)

The panel is fitted to the drawing area, so k is whatever fits, not a standard scale. The title block prints 1 ÷ k rounded to a whole number: a panel drawn at 1:9.4 is labeled 1:9. Measuring off the sheet with the printed scale is off by that rounding.

  • framing.js
  • lines 344 to 345
  • BimCraft 0.8.1
const pw = panel.width || 1000, ph = panel.height || 1000;
const k = Math.min(draw.w / pw, draw.h / ph), ox = draw.x + (draw.w - pw * k) / 2, oy = draw.y + (draw.h - ph * k) / 2;

Scale text in the title block

64-bit

The line of the title block that states the scale and the piece count.

text = "1:" + max(1, round(1 ÷ k))

The whole-number rounding of the scale happens here. The overall dimensions printed on the sheet are the panel's width and height rounded to whole millimeters.

  • framing.js
  • line 382
  • BimCraft 0.8.1
const foot = [project, `${w.scale} 1:${Math.max(1, Math.round(1 / k))}`, `${panel.summary.pieces} ${w.pieces}`, date].filter(Boolean).join('   ');

In detail

Method
Studs on a spacing grid from one end, king and jack studs at each opening, headers on the jacks, cripples on the grid; joists and rim joists for a floor; rafter pairs and a ridge board for a gable roof, with rafter length = slope length × (1 + overhang ÷ run). Lumber sizes are a table of dressed sizes in millimeters.
Number types
64-bit numbers in millimeters, rounded to 0.1 mm on storing.
Tolerances
1e-6 mm when a stud is tested against an opening band or the panel ends. Jacks, cripples and blocking shorter than 1 mm are left out. A wall stud spacing above 610 mm raises a warning.
Limits
The header says the defaults are common starting values, not a code check. Timber weight uses one density, 500 kg/m³. The scale printed on the sheet is the fit-to-page scale rounded to a whole number.

Approach: 64-bit, direct

Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.

Every policy that takes this approach