Resources Numerical accuracy Calculation policy

Dormer geometry

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.

res-dormer.js BimCraft 0.8.1 64-bit 5 calculations Reviewed

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

Default dormer sizes

64-bit

The width, height, pitches, overhang and wall thickness a new dormer starts with.

gable pitch = atan(8 ÷ 12) in degrees; shed pitch = atan(3 ÷ 12) in degrees; sizes = inches × 25.4

The two default pitches are computed with Math.atan and divided by π ÷ 180, not typed as rounded degrees. The lengths are exact products of 25.4.

  • res-dormer.js
  • line 44
  • BimCraft 0.8.1
export const DORMER_DEFAULTS = { kind: 'gable', width: 6 * 12 * IN, height: 4 * 12 * IN, pitch: Math.atan(8 / 12) / D2R, shedPitch: Math.atan(3 / 12) / D2R, overhang: 6 * IN, wall: 5.5 * IN };

Dormer planes, walls and cut

64-bit

Every polygon of the dormer in 3D and the outline of the hole in the roof.

t_v = T × √(1 + s_m²); shed: y_back = (H + t_vd) ÷ (s_m − s_d); gable: y_ridge = (H + s_d × w ÷ 2) ÷ s_m

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.

Every policy that takes this approach