Resources Numerical accuracy Calculation policy

Average daylight factor of rooms

Works out each room's average daylight factor from its windows, its inside surfaces and the angle of sky each window sees.

How the precision is kept

Geometry is held as 64-bit triangles in meters and every step is ordinary 64-bit arithmetic. The sky angle is measured on the model by cutting triangles with a vertical plane, with one absolute tolerance of 1e-6 m. The method sets the accuracy: the window's facing direction is found by a search to 0.1°, the sky angle is the mean of five sections across the window, and the formula is an early-design estimate.

Calculations under this policy

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

daylight-bim.js BimCraft 0.8.1 64-bit 13 calculations Reviewed

Tests tools/bimcraft-test/extra2-daylight.mjs

Total blocked angle

64-bit

The total of the angle intervals that obstructions hide, with overlaps counted once.

total = Σ (hiᵢ − loᵢ) over the merged intervals, each clipped to 0°…90°

Intervals are clipped to 0° to 90°, sorted and merged. Two intervals join when the gap between them is 1e-9 degrees or less (absolute). The total is a sum of the merged lengths in order.

  • daylight-bim.js
  • lines 43 to 49
  • BimCraft 0.8.1
/** Merge intervals and return their total length (degrees), clipped to [lo, hi]. */
function covered(list, lo = 0, hi = 90) {
  const a = list.map(([x, y]) => [Math.max(lo, Math.min(x, y)), Math.min(hi, Math.max(x, y))]).filter(([x, y]) => y > x).sort((p, q) => p[0] - q[0]);
  const out = [];
  for (const iv of a) { const last = out[out.length - 1]; if (last && iv[0] <= last[1] + 1e-9) last[1] = Math.max(last[1], iv[1]); else out.push([...iv]); }
  return { total: out.reduce((s, [x, y]) => s + (y - x), 0), merged: out };
}

Sky angle in one section

64-bit

The angle of sky, from the horizon to straight up, that is still visible from a point on the window's outer face.

cut point = a + t · (b − a), t = d_a ÷ (d_a − d_b); angle = atan2(v, u); θ = 90° − blocked total

Each triangle is cut by the vertical plane through the window. A corner within 1e-6 m of the plane (absolute) counts as on it. A crossing is interpolated only when the two distances have opposite signs beyond that tolerance, so d_a − d_b adds two magnitudes and cannot cancel. A point within 1e-6 m of the window face is taken as straight up or straight down instead of going through atan2.

  • daylight-bim.js
  • lines 51 to 93
  • BimCraft 0.8.1
export function sectionTheta(origin, n, tris) {
  const up = [0, 0, 1], s = [n[1], -n[0], 0];   // the section plane holds n and up; s is square to it
  const [ox, oy, oz] = origin;
  const blocked = [];
  for (let i = 0; i + 8 < tris.length; i += 9) {
    // signed distance of each corner from the plane, and where it lies in the section (u outward, v up)
    const P = [[tris[i], tris[i + 1], tris[i + 2]], [tris[i + 3], tris[i + 4], tris[i + 5]], [tris[i + 6], tris[i + 7], tris[i + 8]]];
    const d = P.map((p) => (p[0] - ox) * s[0] + (p[1] - oy) * s[1]);
    if ((d[0] > EPS && d[1] > EPS && d[2] > EPS) || (d[0] < -EPS && d[1] < -EPS && d[2] < -EPS)) continue;
    const uv = (p) => [(p[0] - ox) * n[0] + (p[1] - oy) * n[1], p[2] - oz];
    const uvs = P.map(uv);
    if (uvs[0][0] <= EPS && uvs[1][0] <= EPS && uvs[2][0] <= EPS) continue;   // all behind the window
    const pts = [];
    for (let k = 0; k < 3; k++) {
      const a = k, b = (k + 1) % 3;
      if (Math.abs(d[a]) <= EPS) pts.push(uvs[a]);
      if ((d[a] > EPS && d[b] < -EPS) || (d[a] < -EPS && d[b] > EPS)) { const t = d[a] / (d[a] - d[b]); pts.push([uvs[a][0] + t * (uvs[b][0] - uvs[a][0]), uvs[a][1] + t * (uvs[b][1] - uvs[a][1])]); }
    }
    if (pts.length < 2) continue;
    // the cut piece (a segment; a triangle lying in the plane gives its extreme points), clipped to u > 0
    let lo = pts[0], hi = pts[0];
    // a point on the window's own face (u = 0, clipped to EPS) is seen straight up or straight down: the exact limit
    const ang = (p) => (p[0] <= EPS * 1.000001 ? (p[1] >= 0 ? Math.PI / 2 : -Math.PI / 2) : Math.atan2(p[1], p[0]));
    // clip each candidate pair to the front half (u ≥ EPS) and take the widest angular span
    const front = [];
    for (let a = 0; a < pts.length; a++) for (let b = a + 1; b < pts.length; b++) {
      let p = pts[a], q = pts[b];
      if (p[0] < EPS && q[0] < EPS) continue;
      if (p[0] < EPS || q[0] < EPS) {
        const t = (EPS - p[0]) / (q[0] - p[0]), m = [EPS, p[1] + t * (q[1] - p[1])];
        if (p[0] < EPS) p = m; else q = m;
      }
      front.push(p, q);
    }
    if (!front.length) continue;
    lo = front[0]; hi = front[0];
    for (const p of front) { if (ang(p) < ang(lo)) lo = p; if (ang(p) > ang(hi)) hi = p; }
    const a0 = ang(lo) * R2D, a1 = ang(hi) * R2D;
    if (a1 > 0 && a0 < 90) blocked.push([a0, a1]);
  }
  const c = covered(blocked);
  return { theta: Math.max(0, 90 - c.total), blocked: c.merged };
}

Mean reflectance of the room

64-bit

The area-weighted mean reflectance of floor, walls, ceiling and glass.

R = (A_floor·ρ_floor + A_wall·ρ_wall + A_ceiling·ρ_ceiling + A_glass·0.1) ÷ (A_floor + A_wall + A_ceiling + A_glass)

A weighted mean in one 64-bit expression of positive terms. The glass reflectance is the constant 0.1; the other three are inputs. A room with no area returns 0 instead of dividing by zero.

  • daylight-bim.js
  • lines 95 to 99
  • BimCraft 0.8.1
// ---------------------------------------------------------------- the formula
export function meanReflectance(areas, refl = DEFAULTS.refl) {
  const A = areas.floor + areas.wall + areas.ceiling + areas.glass;
  return A > 0 ? (areas.floor * refl.floor + areas.wall * refl.wall + areas.ceiling * refl.ceiling + areas.glass * GLASS_R) / A : 0;
}

Average daylight factor

64-bit

The room's average daylight factor in percent.

ADF = Σ (T × A_g × θ × M) ÷ (A × (1 − R²))

The window terms are added in order and divided once. The function returns 0 when A is not positive or R is 1 or more, so the divisor is never zero. θ enters in degrees, as the published formula takes it.

  • daylight-bim.js
  • lines 100 to 105
  • BimCraft 0.8.1
export function averageDF({ A, R, windows }, { T = DEFAULTS.T, M = DEFAULTS.M } = {}) {
  if (!(A > 0) || !(R < 1)) return 0;
  let s = 0;
  for (const w of windows) s += T * w.glazed * w.theta * M;
  return s / (A * (1 - R * R));
}

Triangle area and tilt

64-bit

The area of one triangle of a room's shell and how far its normal points up or down.

area = |AB × AC| ÷ 2; nz = (AB × AC)_z ÷ |AB × AC|

The edge vectors are differences from the first corner, so the size of the model coordinates drops out before the cross product. The length is Math.hypot of the three components. A triangle with no area gives nz = 0 instead of dividing by zero.

  • daylight-bim.js
  • lines 121 to 125
  • BimCraft 0.8.1
function triArea(tr, i) {
  const ax = tr[i + 3] - tr[i], ay = tr[i + 4] - tr[i + 1], az = tr[i + 5] - tr[i + 2], bx = tr[i + 6] - tr[i], by = tr[i + 7] - tr[i + 1], bz = tr[i + 8] - tr[i + 2];
  const cx = ay * bz - az * by, cy = az * bx - ax * bz, cz = ax * by - ay * bx, l = Math.hypot(cx, cy, cz);
  return { area: l / 2, nz: l > 0 ? cz / l : 0 };
}

Closest point of a triangle

64-bit

The point of a triangle nearest to a given point, used to find which room a window belongs to.

Region tests on dot products of 64-bit vectors, compared against exactly zero with no tolerance (the code's comment cites Ericson, Real-Time Collision Detection 5.1.5). The interior case divides by va + vb + vc, which is not guarded for a triangle with no area.

  • daylight-bim.js
  • lines 126 to 145
  • BimCraft 0.8.1
/** Closest point of a triangle to p (Ericson, Real-Time Collision Detection 5.1.5). */
function closestOnTri(p, a, b, c) {
  const sub = (x, y) => [x[0] - y[0], x[1] - y[1], x[2] - y[2]], dot = (x, y) => x[0] * y[0] + x[1] * y[1] + x[2] * y[2];
  const add = (x, y, t) => [x[0] + y[0] * t, x[1] + y[1] * t, x[2] + y[2] * t];
  const ab = sub(b, a), ac = sub(c, a), ap = sub(p, a);
  const d1 = dot(ab, ap), d2 = dot(ac, ap);
  if (d1 <= 0 && d2 <= 0) return a;
  const bp = sub(p, b), d3 = dot(ab, bp), d4 = dot(ac, bp);
  if (d3 >= 0 && d4 <= d3) return b;
  const vc = d1 * d4 - d3 * d2;
  if (vc <= 0 && d1 >= 0 && d3 <= 0) return add(a, ab, d1 / (d1 - d3));
  const cp = sub(p, c), d5 = dot(ab, cp), d6 = dot(ac, cp);
  if (d6 >= 0 && d5 <= d6) return c;
  const vb = d5 * d2 - d1 * d6;
  if (vb <= 0 && d2 >= 0 && d6 <= 0) return add(a, ac, d2 / (d2 - d6));
  const va = d3 * d6 - d5 * d4;
  if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0) return add(b, sub(c, b), (d4 - d3) / ((d4 - d3) + (d5 - d6)));
  const den = 1 / (va + vb + vc);
  return add(add(a, ab, vb * den), ac, vc * den);
}

Direction a window faces

64-bit

The level direction across which a window's triangles are thinnest.

width(α) = max(x·cos α + y·sin α) − min(x·cos α + y·sin α); the α with the least width

The direction is searched in whole degrees from 0° to 179°, then in steps of 0.1° within one degree either side, so the facing direction is resolved to 0.1° and not finer. A candidate replaces the best only when it is narrower by more than 1e-12 m. The fine loop reaches its angles by adding 0.1 repeatedly.

  • daylight-bim.js
  • lines 156 to 165
  • BimCraft 0.8.1
/** The level direction across which a window is thinnest (its glass faces that way, one way or the other): a search by degree, then by a tenth. */
export function facing(tr) {
  const width = (a) => { const x = Math.cos(a), y = Math.sin(a); let lo = Infinity, hi = -Infinity; for (let i = 0; i < tr.length; i += 3) { const u = tr[i] * x + tr[i + 1] * y; if (u < lo) lo = u; if (u > hi) hi = u; } return hi - lo; };
  let best = 0, bw = Infinity;
  for (let d = 0; d < 180; d++) { const w = width(d * D2R); if (w < bw - 1e-12) { bw = w; best = d; } }
  let fine = best;
  for (let d = best - 1; d <= best + 1; d += 0.1) { const w = width(d * D2R); if (w < bw - 1e-12) { bw = w; fine = d; } }
  const a = fine * D2R;
  return [Math.cos(a), Math.sin(a)];
}

Floor, wall and ceiling areas of a room

64-bit

The inside surface areas of a room from the triangles of its space.

A = A_floor + A_wall + A_ceiling

A triangle whose unit normal has |nz| above 0.7 is floor or ceiling, by whether its centroid lies below the middle height of the room's box; the rest is wall. Areas are added triangle by triangle in the order the geometry gives them.

  • daylight-bim.js
  • lines 195 to 200
  • BimCraft 0.8.1
const areas = { floor: 0, wall: 0, ceiling: 0, glass: 0 }, zMid = (p.box.lo[2] + p.box.hi[2]) / 2;
for (let i = 0; i + 8 < p.tr.length; i += 9) {
  const { area, nz } = triArea(p.tr, i);
  if (Math.abs(nz) > 0.7) { if ((p.tr[i + 2] + p.tr[i + 5] + p.tr[i + 8]) / 3 < zMid) areas.floor += area; else areas.ceiling += area; } else areas.wall += area;
}
return [id, { id, guid: val(l.GlobalId), name: String(val(l.Name) ?? ''), longName: String(val(l.LongName) ?? ''), storey: storeyOf(id), A: areas.floor + areas.wall + areas.ceiling, areas, windows: [] }];

Window area

64-bit

The area of one window and the point on its outer face that the sky angle is measured from.

area = OverallWidth × k × OverallHeight × k, k = file length unit in mm ÷ 1000

The area is OverallWidth × OverallHeight, each converted to meters by the file's length unit. A window without those two values takes the width of its triangles across the facing direction times the height of their box.

  • daylight-bim.js
  • lines 237 to 239
  • BimCraft 0.8.1
const ow = Number(val(l.OverallWidth)), oh = Number(val(l.OverallHeight)), k = mm / 1000;
const area = ow > 0 && oh > 0 ? ow * k * oh * k : (sHi - sLo) * ext[2];
const origin = [c[0] + n[0] * uMax, c[1] + n[1] * uMax, c[2]];

Sky angle of a window

64-bit

The window's sky angle: the mean of five sections across its width.

θ = (θ₁ + θ₂ + θ₃ + θ₄ + θ₅) ÷ 5, at 10, 30, 50, 70 and 90 % of the width

The blocked intervals of every obstruction are gathered per section and merged once, so two obstructions that hide the same angles are not counted twice. The result is a mean of five samples: an obstruction narrower than the spacing between sections is counted by the sections it crosses, not by its true share of the window.

  • daylight-bim.js
  • lines 255 to 261
  • BimCraft 0.8.1
const sections = cuts.map((o2) => {
  const all2 = [];
  for (const tr of tris) all2.push(...sectionTheta(o2, n, tr).blocked);
  const cov = covered(all2);
  return { theta: Math.max(0, 90 - cov.total), blocked: cov.merged };
});
const theta = sections.reduce((t, x) => t + x.theta, 0) / sections.length;

Glass taken out of the walls

64-bit

The glass area of a room and the wall area left beside it.

A_glass = min(Σ window areas, A_wall); A_wall = A_wall − A_glass

The window areas are summed in order, capped at the wall area and subtracted from it. The total inside surface A is not changed by this step.

  • daylight-bim.js
  • lines 268 to 270
  • BimCraft 0.8.1
const g = r.windows.reduce((t, x) => t + x.area, 0);
r.areas.glass = Math.min(g, r.areas.wall);
r.areas.wall -= r.areas.glass;

Result per room

64-bit

Each room's mean reflectance, glazed areas, daylight factor and guidance band for the inputs given.

A_g = window area × glazed share; band: below 2 % low, below 5 % mid, else high

Inputs that are not finite numbers fall back to the defaults (T 0.7, M 1, glazed share 0.75). The band limits 2 and 5 are compared on the unrounded daylight factor.

  • daylight-bim.js
  • lines 278 to 289
  • BimCraft 0.8.1
/** The result for these inputs (cheap: run again whenever an input changes). */
export function daylight(g, inputs = {}) {
  const T = Number.isFinite(inputs.T) ? inputs.T : DEFAULTS.T, M = Number.isFinite(inputs.M) ? inputs.M : DEFAULTS.M;
  const share = Number.isFinite(inputs.glazedShare) ? inputs.glazedShare : DEFAULTS.glazedShare;
  const refl = { ...DEFAULTS.refl, ...(inputs.refl || {}) };
  return g.rooms.map((r) => {
    const R = meanReflectance(r.areas, refl);
    const windows = r.windows.map((x) => ({ ...x, glazed: x.area * share }));
    const adf = averageDF({ A: r.A, R, windows }, { T, M });
    return { ...r, R, windows, adf, band: band(adf) };
  });
}

Daylight table as CSV

64-bit

The rooms' results written as a CSV file.

The file holds rounded text: areas to 2 decimals, the mean reflectance to 3, each sky angle to 1 decimal of a degree and the daylight factor to 2. The glazed total of a room is summed in 64-bit before it is rounded.

  • daylight-bim.js
  • lines 292 to 299
  • BimCraft 0.8.1
export function daylightCsv(rooms, { head, storeyName = () => '', inputs = {}, bandName = (b) => b } = {}) {
  const H = head || { room: 'Room', storey: 'Level', A: 'Inside surfaces (m²)', floor: 'Floor (m²)', R: 'Mean reflectance', windows: 'Windows', glazed: 'Glazed area (m²)', theta: 'Sky angle θ (°)', adf: 'Average daylight factor (%)', band: 'Guidance band', inputs: 'Inputs' };   // i18n-ignore: the page passes `head` in the person's language
  const rows = [[H.room, H.storey, H.A, H.floor, H.R, H.windows, H.glazed, H.theta, H.adf, H.band]];
  for (const r of rooms) rows.push([[r.name, r.longName].filter(Boolean).join(' '), storeyName(r.storey), r.A.toFixed(2), r.areas.floor.toFixed(2), r.R.toFixed(3), r.windows.length,
    r.windows.reduce((s, x) => s + x.glazed, 0).toFixed(2), r.windows.map((x) => x.theta.toFixed(1)).join(' '), r.adf.toFixed(2), bandName(r.band)]);
  rows.push([H.inputs, JSON.stringify(inputs)]);
  return '' + rows.map((r) => r.map(cell).join(',')).join('\r\n') + '\r\n';
}

In detail

Method
ADF (%) = T · Ag · θ · M ÷ (A · (1 − R²)), summed over the room's windows. The file's header cites Littlefair, BRE 209, BS 8206-2 and CIBSE LG10. θ is the angle of visible sky in a vertical section square to the window, measured by cutting every other element's triangles with that section.
Number types
Float64Array triangles (9 numbers each, meters, Z up) and ordinary 64-bit numbers.
Tolerances
1e-6 m (absolute) for a point on the section plane or on the window's own face. 1e-9 degrees when blocked angle intervals are merged. 1e-12 m when widths are compared in the facing search. A window belongs to a space within 0.75 m of its shell. A triangle is floor or ceiling when |nz| > 0.7.
Limits
An estimate by a published formula, not a simulation and not a compliance check (the file's header says so). Windows thinner in height than in plan are listed as roof windows and left out. The triangles come from ifc-util.js productTriangles; their precision is set there, not in this file.

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.

Every policy that takes this approach