Resources Numerical accuracy Calculation policy

Manual roof planes

Works out the height of a hand-drawn roof plane at any point, its sloped slab, and its sloped and projected areas.

How the precision is kept

A plane is a plan polygon whose first side is the eave; heights rise inward at tan(pitch). Everything is closed-form 64-bit arithmetic in millimeters and degrees. The polygon's points are stored as text with 3 decimals of the file's length unit and the pitch with 4 decimals of a degree, and the plane is rebuilt from those stored values.

Open item. Store the plane's points with 6 decimals, as its base and thickness already are, instead of 3.

Calculations under this policy

4 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-roofplane.js BimCraft 0.8.1 64-bit 4 calculations Reviewed

Tests tools/bimcraft-test/res-test.mjs, tools/bimcraft-test/modify-test.mjs

Eave frame of a plane

64-bit

Finds the eave direction and the inward direction of a roof plane from its polygon.

u = (p₁ − p₀) ÷ |p₁ − p₀|; n = (−u_y, u_x), flipped when Σ (p − p₀) · n < 0

The inward side is decided by the sign of a sum of signed distances of all points from the eave line, added in order. A zero eave length is replaced by 1.

  • res-roofplane.js
  • lines 27 to 37
  • BimCraft 0.8.1
/** The eave (first side) frame of a polygon: origin p0, u along the eave, n inward (towards the polygon's side). */
export function planeFrame(points) {
  const p0 = points[0], p1 = points[1], L = Math.hypot(p1[0] - p0[0], p1[1] - p0[1]) || 1;
  const u = [(p1[0] - p0[0]) / L, (p1[1] - p0[1]) / L];
  let n = [-u[1], u[0]];
  // inward: towards where most of the polygon lies
  const side = points.reduce((s, p) => s + (p[0] - p0[0]) * n[0] + (p[1] - p0[1]) * n[1], 0);
  // keep n to the left of u (so the frame's Z points up): when the polygon lies to the right, u turns round instead
  if (side < 0) return { o: p0, u: [-u[0], -u[1]], n: [-n[0], -n[1]] };
  return { o: p0, u, n };
}

Height of the plane at a point

64-bit

The height of the plane's underside over a plan point.

z = base + tan θ × ((p − o) · n)

One 64-bit expression using Math.tan of the pitch in radians. The pitch is limited to less than 80 degrees when the plane is placed or changed, not in this function.

  • res-roofplane.js
  • lines 38 to 41
  • BimCraft 0.8.1
export function planeZ(rp, p) {
  const f = planeFrame(rp.points), s = Math.tan(rp.pitch * D2R);
  return rp.base + s * ((p[0] - f.o[0]) * f.n[0] + (p[1] - f.o[1]) * f.n[1]);
}

Polygon area

64-bit

The signed plan area of the plane's polygon.

A = ½ Σ (xᵢ yᵢ₊₁ − yᵢ xᵢ₊₁)

The shoelace sum on the stored millimeter coordinates, added in order. For a polygon far from the origin each product is much larger than the area, so the differences cancel leading digits.

  • res-roofplane.js
  • line 42
  • BimCraft 0.8.1
function area(p) { let s = 0; for (let i = 0; i < p.length; i++) { const a = p[i], b = p[(i + 1) % p.length]; s += a[0] * b[1] - a[1] * b[0]; } return s / 2; }

Sloped slab and areas written to the model

64-bit

Builds the plane's slab and writes its sloped area, projected area, points, pitch, base and thickness.

profile y = ((p − o) · n) ÷ cos θ; depth = thickness ÷ cos θ; GrossArea = projected ÷ cos θ

The sloped area is the projected area divided by cos(pitch). The points are stored as text rounded to 3 decimals of the file's length unit (1 mm in a file in meters) and the pitch to 4 decimals; readRoofPlanes reads those values back, so a later change rebuilds the plane from the rounded points.

  • res-roofplane.js
  • lines 79 to 95
  • BimCraft 0.8.1
const f = planeFrame(rp.points), cos = Math.cos(rp.pitch * D2R), sin = Math.sin(rp.pitch * D2R);
const X = D(f.u[0], f.u[1], 0), Y = D(f.n[0] * cos, f.n[1] * cos, sin);
const Z = [X[1] * Y[2] - X[2] * Y[1], X[2] * Y[0] - X[0] * Y[2], X[0] * Y[1] - X[1] * Y[0]];
const prof = rp.points.map((p) => [(p[0] - f.o[0]) * f.u[0] + (p[1] - f.o[1]) * f.u[1], ((p[0] - f.o[0]) * f.n[0] + (p[1] - f.o[1]) * f.n[1]) / cos]);
const nm = w.L(tr('model.roof.slab'));
const sid = w.make('IFCSLAB', w.guid(), w.ownerHistory(), nm, null, nm, w.H(w.placeAt(host.ObjectPlacement.value, 0, 0, 0, 0)), null, null, 'ROOF');
const solid = w.extrudeAt(w.profile(prof), w.axisFrame(P(f.o[0], f.o[1], rp.base), Z, X), rp.thickness / cos, [0, sin, cos]);
w.styleItem(solid, w.surfaceStyle(tr('model.style.roofing'), [0.42, 0.33, 0.3], 0));
w.setShape(sid, [w.rep('body', 'Body', 'SweptSolid', [solid])]);
let rel = null;
for (const rid of ids(w.api, w.mid, 'IFCRELAGGREGATES')) { const x = w.api.GetLine(w.mid, rid, false); if (x.RelatingObject?.value === rp.id) { rel = rid; break; } }
if (rel) { const x = w.touch(rel); x.RelatedObjects = [w.H(sid)]; w.write(x); }
else w.make('IFCRELAGGREGATES', w.guid(), w.ownerHistory(), null, null, w.H(rp.id), [w.H(sid)]);
const proj = Math.abs(area(rp.points));
w.upsertQto(rp.id, 'Qto_RoofBaseQuantities', [['GrossArea', 'area', proj / cos], ['NetArea', 'area', proj / cos], ['ProjectedArea', 'area', proj]]);
w.upsertPset(rp.id, PLANE_PSET, [['Points', 'IFCTEXT', rp.points.map((p) => round(w.u(p[0]), 3) + ',' + round(w.u(p[1]), 3)).join(';')], ['PitchDegrees', 'IFCREAL', round(rp.pitch, 4)],
  ['Base', 'IFCLENGTHMEASURE', round(w.u(rp.base))], ['Thickness', 'IFCPOSITIVELENGTHMEASURE', round(w.u(rp.thickness))]]);

In detail

Method
z = base + tan(pitch) × distance inward from the eave. Sloped area = projected area ÷ cos(pitch), with the projected area from the shoelace formula.
Number types
64-bit numbers. Degrees are converted with π ÷ 180.
Tolerances
A plane must have at least 3 points and a plan area of at least 1e4 mm². The pitch must be at least 0 and less than 80 degrees.
Limits
The eave direction falls back to a length of 1 when the first two points coincide.

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