Matches each roof edge to the wall under it and builds the roof's sloped slabs, gable walls, fascia boards and soffit panels with their areas and lengths.
How the precision is kept
The roof's faces, ridges, hips and valleys come from roofGeometry in res-roofplan.js; this file turns them into solids and quantities in 64-bit millimeters. Sloped sizes divide by the cosine of the pitch. A gable wall's top is sampled at 65 points along its edge, so its shape is as fine as that sampling. Per-wall pitch and overhang settings are stored as text rounded to 4 and 2 decimals.
Calculations under this policy
7 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.
Finds the drawn wall whose outer face lies along each edge of the building's outline.
d = |cross(û_wall, m − a)|; match when |d − t ÷ 2| ≤ 2 mm
Uses the edge's midpoint and unit directions. The parallel test is |cross| ≤ 1e-4 on unit vectors, the face test is 2 mm absolute, and the best match has the smallest sum of the two misfits. Edges and walls shorter than 1 mm are skipped.
res-roof.js
lines 35 to 56
BimCraft 0.8.1
// ---------------------------------------------------------------------------------------------------- edges and walls
export function edgeWalls(outline, walls) {
return outline.map((a, i) => {
const b = outline[(i + 1) % outline.length], L = Math.hypot(b[0] - a[0], b[1] - a[1]);
if (L < 1) return null;
const u = [(b[0] - a[0]) / L, (b[1] - a[1]) / L], m = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
let best = null;
for (const w of walls) {
const wl = Math.hypot(w.b[0] - w.a[0], w.b[1] - w.a[1]);
if (wl < 1) continue;
const wu = [(w.b[0] - w.a[0]) / wl, (w.b[1] - w.a[1]) / wl];
if (Math.abs(cross(u, wu)) > 1e-4) continue;
const d = Math.abs(cross(wu, sub(m, w.a)));
if (Math.abs(d - w.t / 2) > 2) continue;
const s = dot(wu, sub(m, w.a));
if (s < -w.t || s > wl + w.t) continue;
const score = Math.abs(d - w.t / 2) + Math.max(0, -s, s - wl);
if (!best || score < best.score) best = { guid: w.guid, score };
}
return best ? best.guid : null;
});
}
Per-wall pitch and overhang as stored
64-bit
Writes each wall's roof setting (pitched, gable or none, with its pitch and overhang) as the text kept in the model.
pitch → round(pitch, 4) degrees; overhang → round(overhang, 2) mm
The pitch is rounded to 4 decimals of a degree and the overhang to 2 decimals of a millimeter before they are stored. parseEdges reads them back with Number, so these rounded values are the ones the roof is built from afterwards.
Builds one slab per roof face in the face's own sloped plane.
profile y = ((p − o) · n) ÷ cos θ; depth = thickness ÷ cos θ; sweep direction (0, sin θ, cos θ)
The run from the eave is divided by cos(pitch) to get the distance up the slope, and the plumb thickness is thickness ÷ cos(pitch). There is no guard on the cosine here. The stored PitchDegrees is rounded to 4 decimals.
Builds each gable wall from the top of the wall below up to the underside of the roof.
z sampled at a + (b − a) × k ÷ 64, k = 0 … 64, 1 mm inside the edge
The top of the gable is 65 samples of heightAt, kept where the roof is more than 1 mm above the wall top; points that are collinear within 1e-3 mm² are then removed. A peak that does not fall on a sample is therefore cut off between two samples.
res-roof.js
lines 162 to 174
BimCraft 0.8.1
const a = gb.a, b = gb.b, len = Math.hypot(b[0] - a[0], b[1] - a[1]);
const u = [(b[0] - a[0]) / len, (b[1] - a[1]) / len], n = [-u[1], u[0]];
const wall = L.walls.find((x) => x.guid === kinds[gb.edge].wall) || nearestWall(L.walls, a, b);
const base = wall ? wall.baseOffset + wall.height : top;
const pts = [];
for (let k = 0; k <= 64; k++) {
const tt = k / 64, q = [a[0] + (b[0] - a[0]) * tt + n[0], a[1] + (b[1] - a[1]) * tt + n[1]];
const z = heightAt(plan, q);
if (z !== null && z > base + 1) pts.push([tt * len, z - base]);
}
if (pts.length < 2) continue;
const prof0 = [[pts[0][0], 0], [pts[pts.length - 1][0], 0], ...pts.reverse()];
const prof = prof0.filter((p, i) => i < 2 || i === prof0.length - 1 || Math.abs((prof0[i + 1][1] - p[1]) * (p[0] - prof0[i - 1][0]) - (p[1] - prof0[i - 1][1]) * (prof0[i + 1][0] - p[0])) > 1e-3);
Fascia boards
64-bit
Builds a fascia board along each eave and rake run and totals their length.
length = Σ board lengths; board profile rises by z_b − z_a over its run
Each board's plan length is Math.hypot of its two ends, and the total is the sum of the lengths that roofGeometry reports, added in order. The eave and rake totals are stored rounded to 6 decimals of the file's length unit.
Adds up the area of the level soffit panels under the eaves.
area = Σ |polygon area|
Each panel's area comes from polyArea in res-skeleton.js and the absolute values are added in order. The quantity writer stores areas in square meters rounded to 6 decimals.
res-roof.js
lines 206 to 210
BimCraft 0.8.1
for (const s of plan.soffit) {
// the panel in the walls' plan axes, placed through the walls' storey → the roof's storey
items.push(w.extrudeAt(w.profile(s.poly), w.axisFrame(P(0, 0, s.z - ROOF_DEFAULTS.soffitT), D(0, 0, 1), D(1, 0, 0)), ROOF_DEFAULTS.soffitT));
area += Math.abs(polyArea(s.poly));
}
Roof areas and line lengths written to the model
64-bit
Writes the roof's sloped area, projected area and the lengths of its ridges, hips, valleys, eaves and rakes.
The values are taken as roofGeometry computed them. Areas pass through the quantity writer, which stores square meters rounded to 6 decimals; lengths are rounded to 6 decimals of the file's length unit.
Each face is a polygon in its own sloped plane, swept straight up by thickness ÷ cos(pitch). Gable profiles are heights of the roof's underside sampled along the edge. Soffit area is the sum of the panels' polygon areas.
Number types
64-bit numbers. Degrees are converted with π ÷ 180.
Tolerances
An outline edge matches a wall when |sin of the angle between them| ≤ 1e-4, the edge lies within 2 mm of the wall's face, and its middle is within one wall thickness of the wall's ends. Gable profile points are dropped when collinear within 1e-3 mm². Gable samples count where the roof is more than 1 mm above the wall top.
Limits
A gable's peak that falls between two of the 65 samples is cut off between them. The slab code divides by cos(pitch) and does not itself limit the pitch.
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.