Works out a gable or shed dormer on a roof face: its roof planes, front and cheek walls, where it meets the main roof, and the hole cut in the roof.
How the precision is kept
All heights and distances are closed-form expressions of the roof face's slope, the dormer's pitch, width and wall height in 64-bit millimeters. The dormer's default pitches are computed with Math.atan at full precision. Parts are placed with frames built from cross products and stored through the writer's rounding.
Calculations under this policy
5 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.
Each meeting distance is one division by the main roof's slope, or by the difference of the two slopes for a shed dormer. That difference is required to be above 0.05, which keeps the division well away from 0. The dormer is refused when any of its test points falls outside the roof face.
res-dormer.js
lines 76 to 129
BimCraft 0.8.1
/**
* The dormer's parts in the walls' plan (x, y) and heights (z) — every polygon in 3D. → { faces: [{ pts }] (its roof planes,
* underside), walls: [{ pts }] (vertical polygons: front, cheeks; outer face, each to be swept `wall` thick inward), cut (the
* hole in the roof, plan), footprint (plan, with the overhang), ridge (plan segments), zf, zp, tv } | { error }.
*/
export function dormerGeometry(plan, f, spec0) {
const s = { ...DORMER_DEFAULTS, ...spec0 };
if (!DORMER_KINDS.includes(s.kind)) return { error: 'comforts.err.dormerkind' };
if (!(s.width >= 600 && s.width <= 12000) || !(s.height >= 300 && s.height <= 4000) || !(s.overhang >= 0 && s.overhang <= 1000) || !(s.wall >= 50 && s.wall <= 400)) return { error: 'comforts.err.dormersize' };
const sm = 1 / f.speed, sd = Math.tan(s.pitch * D2R);
if (!(s.pitch > 1 && s.pitch < 80)) return { error: 'model.err.pitch' };
if (s.kind === 'shed' && !(sd < sm - 0.05)) return { error: 'comforts.err.shedpitch' };
const T = plan.thickness, tv = T * Math.sqrt(1 + sm * sm), tvd = T * Math.sqrt(1 + sd * sd);
const X = f.u, Y = f.n, O = s.at, W = s.width, h = W / 2, tw = s.wall, ov = s.overhang, Hf = s.height;
const P = (x, y) => [O[0] + X[0] * x + Y[0] * y, O[1] + X[1] * x + Y[1] * y];
const P3 = (x, y, z) => [...P(x, y), z];
const zf = faceZ(f, O) + tv, zp = zf + Hf; // the roof's top at the front; the dormer's eave (its walls' top)
const out = { faces: [], walls: [], ridge: [], zf, zp, tv, tvd, kind: s.kind, spec: s };
let yBack, cut, foot;
if (s.kind === 'shed') {
const yb = Hf / (sm - sd), yt = (Hf + tvd) / (sm - sd);
yBack = yt;
// its roof: one plane through the front wall's top, rising at its pitch, run back until its top meets the roof
out.faces.push({ pts: [P3(-h - ov, -ov, zp - sd * ov), P3(h + ov, -ov, zp - sd * ov), P3(h + ov, yt, zp + sd * yt), P3(-h - ov, yt, zp + sd * yt)] });
// cheeks: between the roof's underside and the dormer's roof underside
const yc = (Hf + tv) / (sm - sd);
for (const x of [-h, h - tw]) out.walls.push({ side: true, x, pts: [[0, zf - tv], [yc, zp + sd * yc], [0, zp]] });
out.walls.push({ front: true, pts: [[-h, zf - tv], [h, zf - tv], [h, zp], [-h, zp]] });
cut = [P(-h + tw, tw), P(h - tw, tw), P(h - tw, yb), P(-h + tw, yb)];
foot = [P(-h - ov, -ov), P(h + ov, -ov), P(h + ov, yt), P(-h - ov, yt)];
} else {
const yr = (Hf + sd * h) / sm; // the ridge's underside meets the roof's top
const ytAt = (x) => (Hf + tvd + sd * (h - Math.abs(x))) / sm; // where a plane's top meets the roof's top, at x
yBack = ytAt(0);
const zr = zp + sd * h;
for (const sg of [-1, 1]) {
const xe = sg * (h + ov), ze = zp - sd * ov;
const pts = [P3(xe, -ov, ze), P3(0, -ov, zr), P3(0, ytAt(0), zr), P3(xe, ytAt(h + ov), ze)];
out.faces.push({ pts: sg < 0 ? pts : pts.slice().reverse() });
}
out.ridge.push([...P(0, -ov), ...P(0, ytAt(0))]);
const yc = (Hf + tv) / sm;
for (const x of [-h, h - tw]) out.walls.push({ side: true, x, pts: [[0, zf - tv], [yc, zp], [0, zp]] });
out.walls.push({ front: true, pts: [[-h, zf - tv], [h, zf - tv], [h, zp], [0, zr], [-h, zp]] });
const yi = (Hf + sd * tw) / sm;
cut = [P(-h + tw, tw), P(h - tw, tw), P(h - tw, yi), P(0, yr), P(-h + tw, yi)];
foot = [P(-h - ov, -ov), P(h + ov, -ov), P(h + ov, ytAt(h + ov)), P(0, ytAt(0)), P(-h - ov, ytAt(h + ov))];
}
// it must stand on this one face (the eave, hips and valleys of the roof stay where they are)
if (![...cut, P(-h, 0), P(h, 0), P(0, yBack * 0.999)].every((q) => inPoly(q[0], q[1], f.poly))) return { error: 'comforts.err.dormerfit' };
out.cut = cut; out.footprint = foot; out.X = X; out.Y = Y; out.O = O; out.yBack = yBack;
out.cutZ = [zf - tv - 50, zf + sm * yBack + tv + 50];
return out;
}
A sloped polygon as a solid
64-bit
Sets up the frame and profile of a planar polygon swept straight up.
n = unit((p₁ − p₀) × (p₂ − p₀)); x = unit(Z × n); y = n × x; sweep = (Z·x, Z·y, Z·n)
The normal comes from the first three points only, so the polygon is assumed planar. The points are projected on the frame after the first point is subtracted. A horizontal polygon, where Z × n is shorter than 1e-9, uses the X axis.
res-dormer.js
lines 177 to 188
BimCraft 0.8.1
/** A planar polygon (3D, the storey's axes) swept straight up by `depth`: the frame on its plane, the profile in it. */
function planarUp(w, pts, depth) {
let n = unit3(cross3(sub3(pts[1], pts[0]), sub3(pts[2], pts[0])));
if (n[2] < 0) n = n.map((v) => -v);
let hx = unit3(cross3([0, 0, 1], n));
if (Math.hypot(...cross3([0, 0, 1], n)) < 1e-9) hx = [1, 0, 0];
const hy = cross3(n, hx), o = pts[0];
let prof = pts.map((p) => [dot3(sub3(p, o), hx), dot3(sub3(p, o), hy)]);
if (area2(prof) < 0) prof = prof.reverse();
const dir = [dot3([0, 0, 1], hx), dot3([0, 0, 1], hy), dot3([0, 0, 1], n)];
return w.extrudeAt(w.profile(prof), w.axisFrame(o, n, hx), depth, dir);
}
From the walls' level to the roof's level
64-bit
Moves the dormer's points and directions into the axes of the level that holds the roof.
M = roof level⁻¹ × walls level; direction = M·v − M·0
One matrix inverse and product. A direction is transformed as a point and the transformed origin is subtracted, which for levels far from the origin subtracts two large nearly equal numbers.
res-dormer.js
lines 206 to 208
BimCraft 0.8.1
const M = mul4(inv4(w.storeyWorld(roofSt)), w.storeyWorld(rp.wallsLv.id));
const P = (q) => apply4(M, q[0], q[1], q[2]);
const D = (v) => { const a = apply4(M, v[0], v[1], v[2]), o = apply4(M, 0, 0, 0); return [a[0] - o[0], a[1] - o[1], a[2] - o[2]]; };
Hole in the roof slab
64-bit
The outline and depth of the opening that the dormer cuts in its roof face.
outline = M_slab · cut; depth = cutZ₁ − cutZ₀
The cut outline is moved into the roof slab's own axes with a product of two matrices and one inverse. Only the x and y of the transformed points are kept for the profile, and the opening's base height is the transformed z of the cut's lower limit.
res-dormer.js
lines 248 to 250
BimCraft 0.8.1
const Ms = mul4(inv4(w.relMatrix(host.ObjectPlacement.value, sl.ObjectPlacement.value)), M); // walls' plan → the slab's own axes
const pts = G.cut.map((q) => { const r = apply4(Ms, q[0], q[1], 0); return [r[0], r[1]]; });
const z0 = apply4(Ms, 0, 0, G.cutZ[0])[2];
In detail
Method
Slopes as rise over run: main roof s_m = 1 ÷ face speed, dormer s_d = tan(pitch). Vertical thickness = T × √(1 + s²). Meeting lines from equal heights of the two planes. Planar parts swept straight up from a frame on their own plane.
Number types
64-bit numbers. Default sizes in inches times 25.4, which is exact.
Tolerances
A shed dormer needs s_d < s_m − 0.05. Pitch must be between 1° and 80°. A plane's normal counts as vertical when its cross product with Z is shorter than 1e-9. The fit test checks the hole's corners and three more points against the face outline. The change key rounds numbers to 0.01 mm.
Limits
The hole through the roof is cut 50 mm beyond the roof's thickness on both sides.
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.