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