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