Resources Numerical accuracy Calculation policy

Regulation limits read from the pack's tables

Looks up the footing size, stud spacing and height, header span, joist and rafter span and wall insulation a regulation pack asks for, and compares the model with them.

How the precision is kept

Limits come from printed tables carried by the pack, in inches and feet, converted with the exact factors 25.4 and 304.8. A table gives a value per row and column, so the limit is the table's, taken at the next listed column or interpolated linearly between building widths. The insulation R-value uses a conversion constant written with 7 digits.

Open item. Write the R-value conversion as 5.678263337 so the constant is not the limit of the result.

Calculations under this policy

10 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/regs/lib/check.js BimCraft 0.8.1 64-bit 10 calculations Reviewed

Tests tools/bimcraft-test/regs-test.mjs

Footing width and thickness from the table

64-bit

The footing size the pack's table gives for the number of stories, the foundation type, the snow load and the soil pressure.

width = inches × 25.4; thickness = inches × 25.4

The story count is rounded and held between 1 and 3, the load row is the first one at or above the snow load, and the column is the first soil pressure at or above 1500 psf. The cell text 'width x thickness' is parsed and multiplied by the exact 25.4; a thickness above 24 inches is treated as a misprint and not used.

  • addons/regs/lib/check.js
  • lines 305 to 321
  • BimCraft 0.8.1
/** The table's width × thickness (mm) for this footing, or null. */
export function footingFromTable(profile, { stories = 1, foundation = 'slab-on-grade', heavy = false, snowPsf = null, soilPsf = 1500 } = {}) {
  const tables = Object.values(profile?.tables || {}).filter((t) => t.kind === 'footing');
  const t = tables.find((x) => (heavy ? /concrete|masonry/i : /light frame$/i).test(String(x.data?.construction || ''))) || tables[0];
  if (!t || !Array.isArray(t.data?.rows)) return null;
  const loads = [...new Set(t.data.rows.map((r) => Number(r.load)))].sort((a, b) => a - b);
  // the "20" row is 20 psf roof live or 25 psf ground snow; a larger snow load takes the next row up
  const want = snowPsf === null ? loads[0] : (loads.find((l) => (l === 20 ? 25 : l) >= snowPsf) ?? null);
  if (want === null) return null;
  const n = Math.max(1, Math.min(3, Math.round(stories)));
  const row = t.data.rows.find((r) => Number(r.load) === want && Number(r.stories) === n && r.foundation === foundation);
  const col = (t.data.columns_psf || []).findIndex((c) => Number(c) >= soilPsf);
  const cell = row && col >= 0 ? String(row.cells[col] || '') : '';
  const m = /^(\d+(?:\.\d+)?)\s*x\s*(\d+(?:\.\d+)?)$/.exec(cell);
  if (!m || +m[2] > 24) return null;   // a printed typo (e.g. "20x60") is not used
  return { width: +m[1] * IN_MM, thick: +m[2] * IN_MM, table: t, load: want };
}

Footing depth below grade

64-bit

The depth of the footing's underside below grade compared with the rule and the frost depth.

required = max(rule, frost depth); found = max(0, depth below the floor − 6 × 25.4)

Grade is taken as 6 inches (exactly 152.4 mm) below the floor. Both sides are plain 64-bit maxima and one subtraction.

  • addons/regs/lib/check.js
  • lines 340 to 341
  • BimCraft 0.8.1
const need = Math.max(rd.value, frost || 0);
out.push(row('foundation', where, synth('found.footing.depth.min', need, frost && frost > rd.value ? rd.clause + ' / frost depth' : rd.clause, rd.label, rd.note), Math.max(0, below - 6 * IN_MM), ref));

Feet and inches in a table cell

64-bit

Turns a span written as feet-inches in a table into millimeters.

mm = (feet × 12 + inches) × 25.4

Only the whole feet and whole inches at the start of the cell are read; the factors 12 and 25.4 are exact.

  • addons/regs/lib/check.js
  • line 364
  • BimCraft 0.8.1
const ftIn = (s) => { const m = /^(\d+)-(\d+)/.exec(String(s || '')); return m ? (+m[1] * 12 + +m[2]) * IN_MM : null; };

Header span from the table

64-bit

The allowed span of a header for its size, what it carries, the snow load and the building width.

span = a + (b − a) × (w − lo) ÷ (hi − lo), w = building width ÷ 304.8

The building width is converted to feet with the exact 304.8 and the span is interpolated linearly between the two nearest listed widths; when they coincide the cell is used as is. A width more than 1e-6 ft beyond the widest column is reported as beyond the table and not judged.

  • addons/regs/lib/check.js
  • lines 365 to 383
  • BimCraft 0.8.1
export function headerSpanFromTable(profile, { size, floorsAbove = 0, snowPsf = null, widthMm }) {
  const t = Object.values(profile?.tables || {}).find((x) => x.kind === 'header' && /R602\.7\(1\)|exterior/i.test(x.id + ' ' + (x.label || '')))
    || Object.values(profile?.tables || {}).find((x) => x.kind === 'header');
  if (!t || !Array.isArray(t.data?.rows)) return null;
  const supporting = floorsAbove <= 0 ? /^roof and ceiling$/i : floorsAbove === 1 ? /one center-bearing floor/i : /two center-bearing floors/i;
  const r = t.data.rows.find((x) => supporting.test(String(x.supporting || '')) && String(x.size) === size);
  if (!r) return null;
  const cols = Object.keys(r.cells || {}).map((k) => [parseFloat(k), k]).sort((a, b) => a[0] - b[0]);
  const s = snowPsf === null ? cols[0] : cols.find(([v]) => v >= Math.max(30, snowPsf));
  if (!s) return null;
  const byW = r.cells[s[1]], ws = Object.keys(byW).map(Number).sort((a, b) => a - b);
  const wFt = widthMm / FT_MM;
  if (wFt > ws[ws.length - 1] + 1e-6) return { beyond: true, table: t };
  const lo = [...ws].reverse().find((x) => x <= wFt) ?? ws[0], hi = ws.find((x) => x >= wFt) ?? ws[ws.length - 1];
  const a = ftIn(byW[String(lo)]), b = ftIn(byW[String(hi)]);
  if (a === null || b === null) return null;
  const span = hi === lo ? a : a + (b - a) * (wFt - lo) / (hi - lo);
  return { span, table: t, snowCol: s[0] };
}

Span of a header in the model

64-bit

The span of a framed header compared with the table.

span = max(0, length − 3 × 25.4)

The header's stored length less 3 inches of bearing (exactly 76.2 mm), in one 64-bit expression.

  • addons/regs/lib/check.js
  • line 428
  • BimCraft 0.8.1
const span = Math.max(0, (p.length || 0) - 3 * IN_MM);

Stud height and spacing from the table

64-bit

The greatest stud height and spacing the table allows for a stud size, bearing or not, and the floors above.

height = feet × 304.8; spacing = inches × 25.4

The table's feet and inches are multiplied by the exact 304.8 and 25.4. A null cell stays null, which the caller reports as not permitted.

  • addons/regs/lib/check.js
  • lines 384 to 391
  • BimCraft 0.8.1
export function studFromTable(profile, { size, bearing, floorsAbove = 0 }) {
  const t = Object.values(profile?.tables || {}).find((x) => x.kind === 'stud');
  const r = t?.data?.rows?.find((x) => x[0] === size);
  if (!r) return null;
  if (!bearing) return { height: r[6] === null ? null : r[6] * FT_MM, spacing: r[7] === null ? null : r[7] * IN_MM, table: t };
  const sp = floorsAbove <= 0 ? r[2] : floorsAbove === 1 ? r[3] : r[4];
  return { height: r[1] === null ? null : r[1] * FT_MM, spacing: sp === null ? null : sp * IN_MM, table: t };
}

Joist and rafter span from the table

64-bit

The allowed span of a joist or rafter size at its spacing.

column = first listed spacing ≥ spacing ÷ 25.4 − 0.05; span = least span of the listed species

The spacing in millimeters is divided by the exact 25.4 and matched to the next listed spacing with a slack of 0.05 inch. The limit is the minimum over the table's rows for that size, each parsed from feet-inches text.

  • addons/regs/lib/check.js
  • lines 439 to 452
  • BimCraft 0.8.1
// Floor joists: the pack's joist table (kind 'joist', data.rows = [{ species, grade, size, spans: { '16': '15-7', … } }], the 40 psf
// living-area table, the stricter of the two). BimCraft does not record the species, so the span asked for is the LOWEST of the
// table's species for that size and spacing (between listed spacings the next wider one). Joists framed as TJI (engineered) are the
// maker's tables, not the code's, and are left out. A joist's span = its length (the framing tool runs joists wall to wall).
export function spanFromTable(profile, kind, { size, spacingMm }) {
  const t = Object.values(profile?.tables || {}).find((x) => x.kind === kind);
  const rows = (t?.data?.rows || []).filter((r) => r.size === size);
  if (!rows.length) return null;
  const sps = Object.keys(rows[0].spans || {}).map(Number).sort((a, b) => a - b);
  const sIn = spacingMm / IN_MM, col = sps.find((x) => x >= sIn - 0.05);
  if (col === undefined) return null;
  const spans = rows.map((r) => ftIn(r.spans[String(col)])).filter((v) => v !== null);
  return spans.length ? { span: Math.min(...spans), table: t, spacing: col } : null;
}

Horizontal span of a rafter

64-bit

The span of the longest rafter or ceiling joist measured on the horizontal, compared with the table.

span = sloped length × cos(pitch × π ÷ 180)

The longest piece's stored length is multiplied by the cosine of the roof pitch in degrees, with π ÷ 180 from Math.PI. Ceiling joists use their length unchanged. A rafter under a ground snow load above 20 psf is listed but not judged.

  • addons/regs/lib/check.js
  • lines 483 to 502
  • BimCraft 0.8.1
function roofSpanRows(framings, profile, { ruleId, kind, role, levelName, roomFor }) {
  const r = profile?.rule(ruleId);
  if (!r || !framings?.length) return [];
  const crit = profile.criteria || {};
  const snow = crit.ground_snow_load && crit.ground_snow_load.unit === 'psf' ? Number(crit.ground_snow_load.value) : null;
  const out = [];
  for (const f of framings) {
    const ps = f.pieces.filter((p) => p.role === role && /^\d+x\d+$/.test(p.size));
    if (!ps.length) continue;
    const longest = ps.reduce((a, b) => (b.length > a.length ? b : a));
    const horiz = role === 'rafter' ? longest.length * Math.cos((Number(f.pitch) || 0) * RAD) : longest.length;
    const where = (levelName(f.storey) || '') + ' roof ' + (role === 'rafter' ? 'rafters ' : 'ceiling joists ') + longest.size;
    const jt = spanFromTable(profile, kind, { size: longest.size, spacingMm: f.spacing });
    const rule = { ...r, value: jt ? jt.span : r.value, clause: jt ? jt.table.clause || jt.table.id : r.clause };
    const rw = row('roof', where + (jt ? ' at ' + jt.spacing + ' in' : ''), rule, horiz, longest.guid || f.guid);
    if (!jt || (role === 'rafter' && snow !== null && snow > 20)) { rw.ok = null; rw.found = null; }
    out.push(rw);
  }
  return out;
}

Insulation R-value of a wall type

64-bit

The nominal R-value of a wall type's cavity and continuous insulation layers, in imperial units.

R = thickness (mm) ÷ 1000 ÷ λ × 5.678263

Each insulation layer's thickness is divided by its conductivity and multiplied by 5.678263, a conversion constant written with 7 digits, so the result agrees with the full conversion to about 7 digits. Layers are summed in the order they are listed.

  • addons/regs/lib/check.js
  • lines 532 to 545
  • BimCraft 0.8.1
/** A wall type's insulation: { cavity, cont } in R-IP, or null when no layer is insulation (or has no known conductivity). */
export function insulationOf(type, lambdaOf) {
  if (!type?.layers) return null;
  let cavity = 0, cont = 0, any = false;
  for (const l of type.layers) {
    if (!(l.function === 'insulation' || IS_INS.test(String(l.mat || '')))) continue;
    const lam = lambdaOf(String(l.mat || ''));
    if (!(lam > 0) || !(l.t > 0)) continue;
    any = true;
    const r = l.t / 1000 / lam * SI_TO_IP_R;
    if (CONT_INS.test(String(l.mat || ''))) cont += r; else cavity += r;
  }
  return any ? { cavity, cont } : null;
}

Wall insulation against the table's options

64-bit

Whether a wall type's insulation meets any of the cavity and continuous options of its climate zone.

met if cavity + 0.05 ≥ option cavity and continuous + 0.05 ≥ option continuous, for any option

The comparison allows 0.05 in R-value on each part, an absolute slack. The found value shown is cavity plus continuous, and the required value shown is the smallest total among the options.

  • addons/regs/lib/check.js
  • lines 569 to 574
  • BimCraft 0.8.1
const need = opts.length ? Math.min(...opts.map((o) => o.cavity + o.cont)) : 0;
const rule = { ...rw, value: need, clause };
if (!opts.length || !ins) { out.push(note(rule, where, a.ref)); continue; }
const met = opts.some((o) => ins.cavity + 0.05 >= o.cavity && ins.cont + 0.05 >= o.cont);
const rowx = row('wall', where, rule, ins.cavity + ins.cont, a.ref);
rowx.ok = met;   // the options are cavity AND continuous pairs: met by any one

In detail

Method
Table lookup by stories, snow load, soil pressure, size and spacing; linear interpolation of header spans between table widths; R = thickness ÷ conductivity.
Number types
64-bit numbers; table cells are text parsed to numbers.
Tolerances
Joist and rafter spacing is matched to a table column with a slack of 0.05 inch; a building width may exceed the widest table column by 1e-6 ft; an insulation option is met with a slack of 0.05 in R-value.
Limits
A span cell is read as whole feet and whole inches. Spans between listed spacings use the next wider spacing, and the lowest span of the table's species is used because the model does not record the species.

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