Resources Numerical accuracy Calculation policy

Doors and windows in walls

Works out where a door or window sits along its wall, whether it fits, the box that cuts the wall, and the sizes and area written with it.

How the precision is kept

Offsets, sills and sizes are 64-bit numbers in millimeters; fitting is a clamp and the checks use small absolute slacks. The cut is a box 10 mm proud of each wall face. Fixed part sizes are whole or simple fractions of an inch times 25.4. Sizes are written rounded to 6 decimals of the file's unit, and read back from those values.

Calculations under this policy

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

model-openings.js BimCraft 0.8.1 64-bit 9 calculations Reviewed

Tests tools/bimcraft-test/model-test.mjs, tools/bimcraft-test/comforts-test.mjs, tools/bimcraft-test/modify-test.mjs

Fitting an opening in a wall

64-bit

The offset along the wall at which an opening of a given width fits.

offset = max(gap, min(len − width − gap, offset))

A clamp between the gap and the wall's length less the width and the gap; it fails only when that range is empty by more than 1e-6 mm.

  • model-openings.js
  • lines 91 to 96
  • BimCraft 0.8.1
/** Where an opening of `width` can go in `wall` (length len): the offset clamped to [gap, len − width − gap]; null if it can't fit. */
export function fitOffset(wall, width, offset, { gap = 0 } = {}) {
  const max = wall.len - width - gap;
  if (max < gap - 1e-6) return null;
  return Math.max(gap, Math.min(max, offset));
}

Sill of a door

64-bit

Whether a door stands on its wall's own level or on an upper level the wall passes.

kept when |level elevation − base elevation − sill| ≤ 1 mm

The sill is kept only when it is within 1 mm of an upper level's height above the wall's level; anything else becomes 0.

  • model-openings.js
  • lines 48 to 57
  • BimCraft 0.8.1
/**
 * 0.7 BIM-MLWALL: a door's sill is 0 — except on a wall that runs up through levels, where a door on an upper level stands on that
 * level's floor: the sill is then that floor's height above the wall's own level. Anything else comes out 0.
 */
export function doorSill(w, host, sill) {
  if (!(sill > 0)) return 0;
  const levels = readLevels(w), base = levels.find((l) => l.id === host.storey);
  if (!base) return 0;
  return wallLevelsPassed(host, levels).some((l) => Math.abs(l.elevation - base.elevation - sill) <= 1) ? sill : 0;
}

Offset, size and sill read back

64-bit

Where an existing door or window sits and how big it is, from the file.

sill = M[14] + base offset; width = stored × unit

The offset comes from the opening's matrix relative to its wall (through hostOffset for a curved wall) and the sill from the matrix's height plus the wall's base offset. Width and height are the stored values, which were rounded to 6 decimals of the file's unit, times the millimeters per unit.

  • model-openings.js
  • lines 76 to 81
  • BimCraft 0.8.1
const M = w.relMatrix(wall.lp, op.ObjectPlacement.value);
const typeId = typeOf(w, id), size = typeId ? familyIndex(w).sizes.get(typeId) : null;
out.push({
  id, guid: val(line.GlobalId), kind: cls === 'IFCDOOR' ? 'door' : 'window', typeKey: ps.TypeKey || '', typeName: val(line.ObjectType) || val(line.Name) || '',
  openingId, opening: val(op.GlobalId), wall: wallId, wallGuid: wall.guid, storey: wall.storey,
  offset: hostOffset(wall, M, Number(ps.Width) * w.unitMm), width: Number(ps.Width) * w.unitMm, height: Number(ps.Height) * w.unitMm, sill: M[14] + wall.baseOffset,

Default offset and height check

64-bit

Where a new opening goes when no offset is given, and the check that it is not taller than the wall.

offset = (len − W) ÷ 2; refused when sill + height > wall height + 1

The default centers the opening: (wall length − width) ÷ 2. The height check allows 1 mm of slack, an absolute value.

  • model-openings.js
  • lines 139 to 141
  • BimCraft 0.8.1
const off = fitOffset(host, W, Number.isFinite(offset) ? offset : (host.len - W) / 2);
if (off === null) throw Object.assign(new Error('model.err.nofit'), { key: 'model.err.nofit' });
if (S + H > host.height + 1) throw Object.assign(new Error('model.err.tootall'), { key: 'model.err.tootall' });

The box that cuts the wall

64-bit

The body of the opening that is subtracted from the wall.

box: x from 0 to width, y from −t ÷ 2 − 10 to t ÷ 2 + 10, z from 0 to height

A straight wall gets a box as wide and high as the opening and 10 mm proud of each face; a curved wall gets the radial outline from openingCut. The model writer rounds the box's coordinates to 6 decimals of the file's unit.

  • model-openings.js
  • lines 186 to 187
  • BimCraft 0.8.1
const cut = openingCut(host, op, PROUD);   // a curved wall: jambs along the radius
w.setShape(opening, [w.rep('body', 'Body', 'SweptSolid', [cut ? w.extrude(w.profile(cut), 0, op.height) : w.box(0, -t / 2 - PROUD, 0, op.width, t / 2 + PROUD, op.height)])]);

Width, height and area written

64-bit

The quantities and sizes written on a door or window.

area = width × height

The area is one product in mm², stored by the model writer in square meters rounded to 6 decimals. The overall width and height are rounded to 6 decimals of the file's unit.

  • model-openings.js
  • lines 199 to 203
  • BimCraft 0.8.1
w.upsertQto(id, door ? 'Qto_DoorBaseQuantities' : 'Qto_WindowBaseQuantities', [['Width', 'length', op.width], ['Height', 'length', op.height], ['Area', 'area', op.width * op.height]]);
w.upsertPset(opening, OPENING_PSET, [['Width', 'IFCPOSITIVELENGTHMEASURE', round(w.u(op.width))], ['Height', 'IFCPOSITIVELENGTHMEASURE', round(w.u(op.height))]]);
// the element's own size attributes follow
const l = w.touch(id);
l.OverallWidth = round(w.u(op.width)); l.OverallHeight = round(w.u(op.height));

Shift of a wall's start along its line

64-bit

How far a wall's start moved along the wall, so hosted openings can keep their place in the world.

shift = (a′ − a) · u

A projection of the move on the wall's old unit direction; a zero-length wall divides by 1 instead.

  • model-openings.js
  • lines 359 to 364
  • BimCraft 0.8.1
/** Hosted openings follow a wall whose start moved along its own line: offsets keep their place in the world. */
export function keepPlaceOnMove(w, wallBefore, aAfter) {
  const L = Math.hypot(wallBefore.b[0] - wallBefore.a[0], wallBefore.b[1] - wallBefore.a[1]) || 1;
  const u = [(wallBefore.b[0] - wallBefore.a[0]) / L, (wallBefore.b[1] - wallBefore.a[1]) / L];
  return (aAfter[0] - wallBefore.a[0]) * u[0] + (aAfter[1] - wallBefore.a[1]) * u[1];
}

Door body

64-bit

The boxes a starter door is built from: frame, leaves, glass.

frame = min(1.5 in, W ÷ 6)

Frame and leaf depths are 1.5 and 1.75 inches times 25.4, both exact; the frame width is the smaller of that and a sixth of the opening. The rest is additions and halvings of the opening's width and height.

  • model-openings.js
  • lines 207 to 245
  • BimCraft 0.8.1
/** A door's boxes in the opening's axes (x along, y across the wall, z up): frame, then leaves (closed) or panels. */
export function doorParts(type, op, t) {
  const W = op.width, H = op.height, f = Math.min(FRAME, W / 6), out = [];
  const box = (x0, y0, z0, x1, y1, z1, style) => out.push({ box: [x0, y0, z0, x1, y1, z1], style });
  const y0 = -t / 2, y1 = t / 2;
  box(0, y0, 0, f, y1, H, 'frame'); box(W - f, y0, 0, W, y1, H, 'frame'); box(f, y0, H - f, W - f, y1, H, 'frame');
  const s = op.face === 'right' ? -1 : 1;   // leaves sit at the face they swing to
  const ly0 = s > 0 ? y1 - LEAF : y0, ly1 = s > 0 ? y1 : y0 + LEAF;
  const inner = W - 2 * f, top = H - f;
  if (type.garage) box(f, -LEAF / 2, 0, W - f, LEAF / 2, top, 'garage');
  else if (type.slide) {
    const half = inner / 2 + 25;
    box(f, -LEAF, 0, f + half, 0, top, 'glass'); box(W - f - half, 0, 0, W - f, LEAF, top, 'glass');
    box(f, -LEAF, 0, f + 40, 0, top, 'leaf'); box(W - f - 40, 0, 0, W - f, LEAF, top, 'leaf');
  } else if (type.pocket) box(f, -LEAF / 2, 0, W - f, LEAF / 2, top, 'leaf');
  else if (type.bifold) box(f, -LEAF / 2, 0, W - f, LEAF / 2, top, 'leaf');   // folded shut in 3D
  else if (type.barn) {
    // a slab hung on the wall face, a little wider than the opening, under its track
    const yb = s > 0 ? y1 : y0 - LEAF, yt = s > 0 ? y1 + LEAF : y0;
    box(-50, yb, 0, W + 50, yt, H + 25, 'leaf');
    box(-50, s > 0 ? yt : yb - 25, H + 40, 2 * W, s > 0 ? yt + 25 : yb, H + 80, 'frame');
  } else if (type.sidelites) {
    const sl = Math.min(type.sidelites, W / 4);
    box(f, -GLASS / 2, 0, f + sl, GLASS / 2, top, 'glass'); box(W - f - sl, -GLASS / 2, 0, W - f, GLASS / 2, top, 'glass');
    box(f + sl, y0, 0, f + sl + f, y1, top, 'frame'); box(W - f - sl - f, y0, 0, W - f - sl, y1, top, 'frame');
    box(f + sl + f, ly0, 0, W - f - sl - f, ly1, top - 3, 'leaf');
  } else if (type.leaves === 2) {
    if (type.glazed) {
      // glass in a rail-and-stile frame on each leaf
      for (const [a, b] of [[f, f + inner / 2 - 2], [f + inner / 2 + 2, W - f]]) {
        const st = 100;
        box(a, ly0, 0, a + st, ly1, top - 3, 'leaf'); box(b - st, ly0, 0, b, ly1, top - 3, 'leaf');
        box(a + st, ly0, 0, b - st, ly1, 200, 'leaf'); box(a + st, ly0, top - 3 - st, b - st, ly1, top - 3, 'leaf');
        box(a + st, (ly0 + ly1) / 2 - GLASS / 2, 200, b - st, (ly0 + ly1) / 2 + GLASS / 2, top - 3 - st, 'glass');
      }
    } else { box(f, ly0, 0, f + inner / 2 - 2, ly1, top - 3, 'leaf'); box(f + inner / 2 + 2, ly0, 0, W - f, ly1, top - 3, 'leaf'); }
  } else box(f, ly0, 0, W - f, ly1, top - 3, 'leaf');
  return out;
}

Window body

64-bit

The boxes a starter window is built from: frame, rails, stiles, glass and an optional well.

frame = min(2 in, W ÷ 6, H ÷ 6)

The frame is the smallest of 2 inches, a sixth of the width and a sixth of the height; the depth is the smaller of 4.5 inches and the wall's thickness. Inch values are multiplied by 25.4.

  • model-openings.js
  • lines 246 to 267
  • BimCraft 0.8.1
/** A window's boxes: frame (at the face it sits on), sashes' rails and stiles, glass. */
export function windowParts(type, op, t) {
  const W = op.width, H = op.height, f = Math.min(WFRAME, W / 6, H / 6), out = [];
  const box = (x0, y0, z0, x1, y1, z1, style) => out.push({ box: [x0, y0, z0, x1, y1, z1], style });
  const d = Math.min(WDEPTH, t), s = op.face === 'right' ? -1 : 1;
  const y0 = s > 0 ? t / 2 - d : -t / 2, y1 = y0 + d;
  box(0, y0, 0, f, y1, H, 'frame'); box(W - f, y0, 0, W, y1, H, 'frame');
  box(f, y0, 0, W - f, y1, f, 'frame'); box(f, y0, H - f, W - f, y1, H, 'frame');
  const gy = (y0 + y1) / 2;
  if (type.rail) { const m = H / 2; box(f, y0 + d * 0.15, m - f / 2, W - f, y1 - d * 0.15, m + f / 2, 'frame'); }
  if (type.stile) { const m = W / 2; box(m - f / 2, y0 + d * 0.15, f, m + f / 2, y1 - d * 0.15, H - f, 'frame'); }
  box(f, gy - GLASS / 2, f, W - f, gy + GLASS / 2, H - f, 'glass');
  if (type.well) {
    const { x0, x1, near, far, bottom, top, gauge } = windowWell(op, t);
    const Y = (v) => s * v;
    const wall = (a, b, c, d) => box(a, Math.min(Y(b), Y(d)), bottom, c, Math.max(Y(b), Y(d)), top, 'frame');
    wall(x0 - gauge, near, x0, far + gauge);
    wall(x1, near, x1 + gauge, far + gauge);
    wall(x0, far, x1, far + gauge);
  }
  return out;
}

In detail

Method
offset clamped to [gap, length − width − gap]; default offset centers the opening; area = width × height.
Number types
64-bit numbers in millimeters.
Tolerances
Fit refused when the room is short by more than 1e-6 mm. Too tall when sill + height exceeds the wall's height by more than 1 mm. A door stands on an upper level when its sill is within 1 mm of that level's height. Width and height under 100 mm are refused.
Checked by
tools/bimcraft-test/model-test.mjs calls placeOpening and changeOpening, and tools/bimcraft-test/modify-test.mjs calls readOpenings, in Node.

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