Resources Numerical accuracy Calculation policy

Regulation checks of rooms, doors, windows and outlets

Works out what the model has for each rule of the active regulation pack: room area and least dimension, window area and openable area as a share of the floor, escape openings, egress door width, outlet spacing along the walls.

How the precision is kept

The arithmetic is 64-bit in millimeters and square millimeters. Several found values are estimates set by the method, not by the arithmetic: a window's clear opening and openable share come from its type name, the ceiling height is the lowest wall on the level, and a door's clear width is the leaf width less 50.8 mm. The schedule shows rounded text; the row keeps the unrounded number.

Calculations under this policy

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

addons/regs/lib/check.js BimCraft 0.8.1 64-bit 9 calculations Reviewed

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

Openable share of a window

Exact

How much of a window opens, from its type name.

fixed, picture, transom: 0; hung: 0.5; anything else: 1

Three fixed factors chosen by matching the type name; no measurement of the sash is involved.

  • addons/regs/lib/check.js
  • lines 32 to 38
  • BimCraft 0.8.1
/** How much of a window opens, by its type key: fixed 0, hung 0.5, anything else 1. */
export function openableShare(typeKey) {
  const k = String(typeKey || '');
  if (/fixed|picture|transom/.test(k)) return 0;
  if (/hung/.test(k)) return 0.5;
  return 1;
}

Net clear opening of a window

64-bit

The clear width, height and area of a window for the escape opening rules, estimated from its type.

hung: w × h ÷ 2; slider: w ÷ 2 × h; other opening types: w × h; fixed: 0

Width and height are read with Number() and multiplied in 64-bit; the halving is exact. Frame and sash thickness are not deducted, so the result is the opening size scaled by the type's factor.

  • addons/regs/lib/check.js
  • lines 60 to 67
  • BimCraft 0.8.1
/** The net clear opening of a window, estimated from its type: { w, h, area } in mm and mm². */
export function clearOpening(o) {
  const k = String(o.typeKey || ''), w = Number(o.width) || 0, h = Number(o.height) || 0;
  if (/fixed|picture|transom/.test(k)) return { w: 0, h: 0, area: 0 };
  if (/hung/.test(k)) return { w, h: h / 2, area: w * h / 2 };
  if (/slider|sliding/.test(k)) return { w: w / 2, h, area: w * h / 2 };
  return { w, h, area: w * h };
}

Least width of a room

64-bit

The narrowest width across a room outline, used for the least room dimension and hall width rules.

min over edges of (max − min of p · n), n = unit normal of the edge

For every edge the outline is projected on the edge's unit normal and the extent is taken; the smallest extent wins. Edges shorter than 1 mm are skipped, which also avoids dividing by a near-zero length.

  • addons/regs/lib/check.js
  • lines 68 to 80
  • BimCraft 0.8.1
/** The narrowest width across a polygon: for each edge direction, the extent across it; the least of those. */
export function leastWidth(poly) {
  let best = Infinity;
  for (let i = 0; i < poly.length; i++) {
    const a = poly[i], b = poly[(i + 1) % poly.length], dx = b[0] - a[0], dy = b[1] - a[1], L = Math.hypot(dx, dy);
    if (L < 1) continue;
    const nx = -dy / L, ny = dx / L;
    let lo = Infinity, hi = -Infinity;
    for (const p of poly) { const d = p[0] * nx + p[1] * ny; if (d < lo) lo = d; if (d > hi) hi = d; }
    best = Math.min(best, hi - lo);
  }
  return Number.isFinite(best) ? best : null;
}

Window area credited to a room

64-bit

Adds each window's area and openable area to the room on either face of its wall.

center = a + u × (offset + width ÷ 2); area = width × height; open = area × share

The window center is placed along the wall's unit direction, and the room is looked up at a point half the wall thickness plus 100 mm to each side. Areas are products of the stored width and height, summed per room in the order the openings are listed.

  • addons/regs/lib/check.js
  • lines 95 to 100
  • BimCraft 0.8.1
const at = (Number(o.offset) || 0) + (Number(o.width) || 0) / 2, cx = w.a[0] + ux * at, cy = w.a[1] + uy * at, d = (w.t || 0) / 2 + 100;
const area = (Number(o.width) || 0) * (Number(o.height) || 0), share = openableShare(o.typeKey), seen = new Set();
for (const side of [1, -1]) {   // the room on either face of the wall (an outside face has none)
  const room = loopAt(L.rooms, [cx - uy * d * side, cy + ux * d * side]);
  if (room && !seen.has(room)) { seen.add(room); const s = slot(room); s.glaze += area; s.open += area * share; s.wins.push(o); }
}

Ceiling height, light and ventilation of a room

64-bit

The values compared with the ceiling height, natural light and ventilation rules.

ceiling = lowest wall height on the level; light % = window area ÷ floor area × 100; vent % = openable area ÷ floor area × 100

The percentages are one division and one multiplication in 64-bit; the floor area is only used when it is greater than zero. The ceiling height is the minimum wall height on the level, since ceilings are not modeled.

  • addons/regs/lib/check.js
  • lines 109 to 110
  • BimCraft 0.8.1
const heights = (snap.walls || []).filter((w) => w.storey === sp.storey).map((w) => wallHeight(w, snap.levels));
const found = { 'room.ceiling.min': heights.length ? Math.min(...heights) : null, 'room.light.min': s.glaze / loop.area * 100, 'room.vent.min': s.open / loop.area * 100 };

Egress door clear width

64-bit

Finds the widest exterior door and its clear width and height for the egress door rules.

clear width = max(0, leaf width − 50.8)

50.8 mm is exactly 2 inches. A door is exterior when exactly one of the two points 100 mm beyond the wall faces lies in a room. The height is the stored door height with no deduction.

  • addons/regs/lib/check.js
  • lines 131 to 153
  • BimCraft 0.8.1
/** The egress door: the widest exterior door (a door with a room on one side only); clear width = leaf width less 2 in (51 mm). */
export function doorRows(snap, loops, profile, { where = 'Egress door' } = {}) {
  if (!profile || !snap) return [];
  const rw = profile.rule('door.egress.width.min'), rh = profile.rule('door.egress.height.min');
  if (!rw && !rh) return [];
  const wallOf = new Map((snap.walls || []).map((w) => [w.id, w]));
  let best = null;
  for (const o of snap.openings || []) {
    if (o.kind !== 'door') continue;
    const w = wallOf.get(o.wall), L = w && loops.get(w.storey);
    if (!L || !w.a || !w.b) continue;
    const dx = w.b[0] - w.a[0], dy = w.b[1] - w.a[1], len = Math.hypot(dx, dy) || 1, ux = dx / len, uy = dy / len;
    const at = (Number(o.offset) || 0) + (Number(o.width) || 0) / 2, cx = w.a[0] + ux * at, cy = w.a[1] + uy * at, d = (w.t || 0) / 2 + 100;
    const sides = [1, -1].map((side) => loopAt(L.rooms, [cx - uy * d * side, cy + ux * d * side]));
    if (sides.filter(Boolean).length !== 1) continue;   // both sides rooms: an inside door; neither: not in a room
    if (!best || o.width > best.width) best = o;
  }
  const ref = best ? best.guid || String(best.id ?? '') : '';
  const out = [];
  if (rw) out.push(row('door', where, rw, best ? Math.max(0, best.width - 50.8) : null, ref));
  if (rh) out.push(row('door', where, rh, best ? best.height : null, ref));
  return out;
}

Position of a point along a room's perimeter

64-bit

The distance along the room outline to the point nearest an outlet or a door, and the outline's total length.

s = Σ earlier edge lengths + t × L; t = clamp(((p − a) · (b − a)) ÷ L², 0, 1)

Edge lengths are square roots of sums of squares, added in order. The nearest point is found on each edge with a clamped parameter; a zero-length edge uses t = 0.

  • addons/regs/lib/check.js
  • lines 194 to 205
  • BimCraft 0.8.1
/** Points along a closed polygon's perimeter: { s (distance from the first vertex), p } for the point nearest to `pt`, and the total. */
function onPerimeter(poly, pt) {
  let best = null, run = 0;
  for (let i = 0; i < poly.length; i++) {
    const a = poly[i], b = poly[(i + 1) % poly.length], dx = b[0] - a[0], dy = b[1] - a[1], L2 = dx * dx + dy * dy, L = Math.sqrt(L2);
    const t = L2 ? Math.max(0, Math.min(1, ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / L2)) : 0;
    const q = [a[0] + dx * t, a[1] + dy * t], d = Math.hypot(pt[0] - q[0], pt[1] - q[1]);
    if (!best || d < best.d) best = { s: run + t * L, d };
    run += L;
  }
  return { s: best ? best.s : 0, d: best ? best.d : Infinity, total: run };
}

Longest distance to an outlet along the wall

64-bit

The greatest distance, measured along the floor line, from any point of a wall space to an outlet in that space.

per wall space: max(first, length − last, half of each gap between neighbors); a space with no doorway: (first + length − last) ÷ 2

Positions are reduced modulo the perimeter length and sorted. Wall spaces shorter than 609.6 mm are ignored and a remainder of 1 mm or less between doorways is not a space; outlets are assigned to a space with a slack of 1e-6 mm. A wall space with no outlet returns infinity, which the caller records as a failed check with no number.

  • addons/regs/lib/check.js
  • lines 207 to 232
  • BimCraft 0.8.1
/**
 * The longest distance, measured along the floor line, from a point of any wall space to an outlet in that space. Wall spaces are the
 * room's perimeter broken at doorways; a wall space shorter than `minSpace` (2 ft) needs none. → { found (mm) | Infinity when a wall
 * space has no outlet, spaces }.
 */
export function wallOutletGap(poly, doorGaps, outlets, { minSpace = 609.6 } = {}) {
  const total = onPerimeter(poly, poly[0]).total;
  const norm = (s) => ((s % total) + total) % total;
  const gaps = doorGaps.map(([a, b]) => [norm(a), norm(b)]).sort((x, y) => x[0] - y[0]);
  const pos = outlets.map(norm).sort((a, b) => a - b);
  // wall spaces: [start, end] along the perimeter between doorways (one cyclic space when there are none)
  const spaces = [];
  if (!gaps.length) spaces.push({ a: 0, b: total, cyclic: true });
  else for (let i = 0; i < gaps.length; i++) { const a = gaps[i][1], b = gaps[(i + 1) % gaps.length][0] + (i + 1 === gaps.length ? total : 0); if (b - a > 1) spaces.push({ a, b }); }
  let worst = 0;
  for (const sp of spaces) {
    const len = sp.b - sp.a;
    if (len < minSpace) continue;
    const inside = pos.flatMap((p) => [p, p + total]).filter((p) => p >= sp.a - 1e-6 && p <= sp.b + 1e-6).map((p) => p - sp.a).sort((x, y) => x - y);
    if (!inside.length) return { found: Infinity, spaces: spaces.length };
    let g = sp.cyclic ? (inside[0] + len - inside[inside.length - 1]) / 2 : Math.max(inside[0], len - inside[inside.length - 1]);
    for (let i = 1; i < inside.length; i++) g = Math.max(g, (inside[i] - inside[i - 1]) / 2);
    worst = Math.max(worst, g);
  }
  return { found: worst, spaces: spaces.length };
}

Numbers in the regulation schedule

64-bit

Formats a found value or a limit for the schedule table.

percent: 1 decimal; m²: 2 decimals; ratio and U-value: 2 decimals; R-value: 1 decimal; count: whole number

The rounding is applied to the text of the schedule only. The same row's found value is also returned unrounded in the schedule's number column.

  • addons/regs/lib/check.js
  • line 164
  • BimCraft 0.8.1
const fmt = (v, unit) => (unit === 'pct' ? (Math.round(v * 10) / 10).toLocaleString(undefined, { maximumFractionDigits: 1 }) + ' %' : unit === 'm2' ? fmtArea(v) : unit === 'count' ? String(Math.round(v)) : unit === 'ratio' ? String(Math.round(v * 100) / 100) : unit === 'r' ? 'R-' + (Math.round(v * 10) / 10) : unit === 'u' ? 'U-' + (Math.round(v * 100) / 100) : fmtLen(v));
addons/regs/lib/route.js BimCraft 0.8.1 64-bit 2 calculations Reviewed

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

Rooms and doors of the escape routes

64-bit

Builds the graph of rooms and the doors that join them or lead outside, with each door's clear width.

clear width = max(0, leaf width − 50.8)

Door sides are found the same way as for the egress door: a point half the wall thickness plus 100 mm to each side of the door's center on the wall. The clear width deducts exactly 2 inches.

  • addons/regs/lib/route.js
  • lines 19 to 43
  • BimCraft 0.8.1
export function routeGraph(snap, loops, { clearOf = (w) => Math.max(0, w - 50.8) } = {}) {
  const rooms = [], doors = [];
  const idOf = new Map();   // room loop → room id
  for (const sp of snap.derived?.spaces || []) {
    const L = loops.get(sp.storey), loop = L && loopAt(L.rooms, sp.seed);
    if (!loop || !(loop.area > 0) || idOf.has(loop)) continue;
    const id = sp.guid || String(sp.id ?? ''), label = [sp.number, sp.name].filter(Boolean).join(' ') || id;
    idOf.set(loop, id);
    rooms.push({ id, label, at: sp.seed ? [sp.seed[0], sp.seed[1], 0] : null, storey: sp.storey });
  }
  const wallOf = new Map((snap.walls || []).map((w) => [w.id, w]));
  for (const o of snap.openings || []) {
    if (o.kind !== 'door') continue;
    const w = wallOf.get(o.wall), L = w && loops.get(w.storey);
    if (!L || !w.a || !w.b) continue;
    const dx = w.b[0] - w.a[0], dy = w.b[1] - w.a[1], len = Math.hypot(dx, dy) || 1, ux = dx / len, uy = dy / len;
    const at = (Number(o.offset) || 0) + (Number(o.width) || 0) / 2, cx = w.a[0] + ux * at, cy = w.a[1] + uy * at, d = (w.t || 0) / 2 + 100;
    const sides = [1, -1].map((side) => { const loop = loopAt(L.rooms, [cx - uy * d * side, cy + ux * d * side]); return loop ? idOf.get(loop) || null : null; });
    const real = sides.filter(Boolean);
    if (!real.length) continue;
    const id = o.guid || String(o.id ?? '');
    doors.push({ id, a: real[0], b: real.length === 2 && real[1] !== real[0] ? real[1] : null, width: clearOf(Number(o.width) || 0), label: 'Door ' + id.slice(0, 8), storey: w.storey, at: [cx, cy, 0] });
  }
  return { rooms, doors };
}

Narrowest door on the way out of a room

64-bit

The finding for each room: the narrowest door on its best way out against the pack's egress width.

utilization = minimum ÷ bottleneck; stated width = round(bottleneck)

The utilization divides the unrounded limit by the unrounded bottleneck width. The widths stored in the finding (found, required and the text values) are rounded to a whole millimeter with Math.round.

  • addons/regs/lib/route.js
  • lines 54 to 57
  • BimCraft 0.8.1
const findings = res.rows.map((r) => ({
  id: 'route:' + r.room, kind: 'route', subject: { kind: 'space', id: r.room, label: r.label }, util: r.reachable && min > 0 ? min / r.bottleneck : null, factor: null, failing: r.failing,
  what: { width: r.bottleneck === null ? null : Math.round(r.bottleneck), need: Math.round(min), door: r.doorLabel || '', reachable: r.reachable ? 'yes' : 'no' },
  req: { op: '>=', value: Math.round(min), unit: 'mm' }, found: { value: r.bottleneck === null ? null : Math.round(r.bottleneck), unit: 'mm' },

In detail

Method
Window and door positions along their wall; rooms found by a point 100 mm beyond each wall face; least width as the smallest extent across any edge direction; outlet spacing measured along the room perimeter between doorways.
Number types
64-bit numbers.
Tolerances
Edges shorter than 1 mm are skipped in the least width. A wall space under 609.6 mm (2 ft) needs no outlet; perimeter positions are compared with a slack of 1e-6 mm; a door is on a room's perimeter within half the wall thickness plus 150 mm. All absolute.
Limits
Window areas are opening width × height, not the glass. The least width of a room that is not convex is the least extent of its whole outline. Escape routes do not follow stairs between levels.

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