Works out a building mass's footprint area, gross floor area, height, floor levels, the ground under it and the cut and fill to set it on the slope.
How the precision is kept
Areas, heights and levels are closed form in 64-bit. The ground under the footprint is sampled on a 40 × 40 grid over its bounds, and the cut and fill are sums over those samples with each sample standing for an equal share of the exact footprint area, so the earthwork follows the sampling.
Calculations under this policy
9 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.
The shoelace sum on absolute project coordinates with no shift to a local origin: each product is about the square of the coordinate size, so with coordinates in the millions the products are near 1e12 to 1e13 and the area is resolved to about a thousandth of a square unit per corner rather than to 15 digits.
massing.js
line 56
CivilCraft 0.8.1
export function signedArea(pts) { let a = 0; for (let i = 0; i < pts.length; i++) { const p = pts[i], q = pts[(i + 1) % pts.length]; a += p.x * q.y - q.x * p.y; } return a / 2; }
Gross floor area and height
64-bit
The floor area of all floors and the height of the mass.
The standard polygon centroid on absolute coordinates; the sums involve cubes of the coordinate size and cancel more heavily than the area does. When |A| is under 1e-12 the mean of the corners is returned instead.
massing.js
lines 60 to 66
CivilCraft 0.8.1
export function centroid(pts) {
const A = signedArea(pts);
if (Math.abs(A) < 1e-12) return { x: pts.reduce((s, p) => s + p.x, 0) / pts.length, y: pts.reduce((s, p) => s + p.y, 0) / pts.length };
let cx = 0, cy = 0;
for (let i = 0; i < pts.length; i++) { const p = pts[i], q = pts[(i + 1) % pts.length], k = p.x * q.y - q.x * p.y; cx += (p.x + q.x) * k; cy += (p.y + q.y) * k; }
return { x: cx / (6 * A), y: cy / (6 * A) };
}
Ground under a mass
64-bit
The lowest, highest and mean ground elevation under the footprint, and the samples used for earthwork.
sample (i, j) at x₀ + (i + 0.5)·dx, y₀ + (j + 0.5)·dy; dA = |A| ÷ samples; mean = Σ z ÷ n
Sample positions come from whole-number indexes. Each sample is given an equal share of the exact footprint area, which corrects the total area but not the shape at the edge. The mean is over grid samples only; the corners add to the lowest and highest. Samples off the surface count in the area share but not in the mean.
massing.js
lines 69 to 94
CivilCraft 0.8.1
/** Ground heights under the footprint: a grid of `cells` × `cells` over its bounds (cells inside it) plus its corners.
* elevAt(x, y) → z | null. Each sample carries the area it stands for (dA), so volumes are sums. */
export function groundUnder(m, elevAt, { cells = 40 } = {}) {
const P = m.pts;
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
for (const p of P) { x0 = Math.min(x0, p.x); y0 = Math.min(y0, p.y); x1 = Math.max(x1, p.x); y1 = Math.max(y1, p.y); }
const nx = cells, ny = cells, dx = (x1 - x0) / nx, dy = (y1 - y0) / ny;
const samples = [];
let missing = 0;
for (let j = 0; j < ny; j++) for (let i = 0; i < nx; i++) {
const x = x0 + (i + 0.5) * dx, y = y0 + (j + 0.5) * dy;
if (!pointInPoly(x, y, P)) continue;
const z = elevAt(x, y);
if (fin(z)) samples.push({ x, y, z }); else missing++;
}
// the cells cover the footprint to within a cell's width; scale their area to the exact footprint area
const A = massArea(m), n = samples.length + missing;
const cellArea = n ? A / n : 0;
for (const s of samples) s.dA = cellArea;
let min = Infinity, max = -Infinity, sum = 0, w = 0;
const corners = P.map((p) => ({ x: p.x, y: p.y, z: elevAt(p.x, p.y) })).filter((c) => fin(c.z));
for (const s of samples) { min = Math.min(min, s.z); max = Math.max(max, s.z); sum += s.z; w++; }
for (const c of corners) { min = Math.min(min, c.z); max = Math.max(max, c.z); }
if (!w) return corners.length ? { min, max, mean: corners.reduce((s, c) => s + c.z, 0) / corners.length, n: corners.length, missing: P.length - corners.length, cellArea, samples: [] } : null;
return { min, max, mean: sum / w, n: w, missing, cellArea, samples };
}
Ground floor level
64-bit
The level the mass is set at: the mean ground, the highest ground (podium) or the lowest (cut in).
A choice among the three sampled values; no arithmetic.
The earth to remove above the ground floor level and to place below it.
fill = Σ (z₀ − z)·dA where z < z₀; cut = Σ (z − z₀)·dA otherwise
Two running sums of same-sign terms over the samples, in 64-bit. The accuracy is that of the 40 × 40 sampling.
massing.js
lines 101 to 105
CivilCraft 0.8.1
export function baseVolumes(m, ground, z0) {
let fill = 0, cut = 0;
for (const s of ground?.samples || []) { if (s.z < z0) fill += (z0 - s.z) * s.dA; else cut += (s.z - z0) * s.dA; }
return { fill, cut };
}
Floor levels
64-bit
The elevation of each floor and its top.
z_i = z₀ + i × floor height
Each level is one multiplication by the whole-number floor index and one addition, not a running sum.
massing.js
lines 115 to 117
CivilCraft 0.8.1
export function levels(m, z0) {
return Array.from({ length: m.floors }, (_, i) => ({ n: i + 1, name: t('ms.level', { n: i + 1 }), z: z0 + i * m.floorHeight, top: z0 + (i + 1) * m.floorHeight, h: m.floorHeight }));
}
Floors for a pulled height
Exact
The floor count nearest to a height set by push and pull.
floors = clamp(round(H ÷ floor height), 1, 200)
The result is a whole number; the height the person pulled to is replaced by floors × floor height.
massing.js
lines 119 to 120
CivilCraft 0.8.1
/** Push/pull: the floor count whose height is nearest to H (at least one floor). */
export function setHeight(m, H) { return Math.max(1, Math.min(MAX_FLOORS, Math.round(H / m.floorHeight))); }
Triangles of a footprint
64-bit
Splits a simple polygon into triangles for the pad top and the roof.
Ear clipping with cross products compared against ±1e-12 (absolute, length squared) on absolute coordinates. When no ear is found the flattest corner is dropped so the loop ends, which leaves out a sliver.
massing.js
lines 123 to 150
CivilCraft 0.8.1
/** Ear clipping of a simple polygon, any winding; triangles as indices into pts, counter-clockwise. */
export function triangulate(pts) {
const n = pts.length;
const idx = pts.map((_, i) => i);
if (signedArea(pts) < 0) idx.reverse();
const out = [];
const cross = (a, b, c) => (pts[b].x - pts[a].x) * (pts[c].y - pts[a].y) - (pts[b].y - pts[a].y) * (pts[c].x - pts[a].x);
const inTri = (p, a, b, c) => cross(a, b, p) >= -1e-12 && cross(b, c, p) >= -1e-12 && cross(c, a, p) >= -1e-12;
let guard = 0;
while (idx.length > 3 && guard++ < n * n) {
let clipped = false;
for (let k = 0; k < idx.length; k++) {
const a = idx[(k + idx.length - 1) % idx.length], b = idx[k], c = idx[(k + 1) % idx.length];
if (cross(a, b, c) <= 1e-12) continue; // reflex or flat
let ear = true;
for (const p of idx) { if (p === a || p === b || p === c) continue; if (inTri(p, a, b, c)) { ear = false; break; } }
if (!ear) continue;
out.push(a, b, c); idx.splice(k, 1); clipped = true; break;
}
if (!clipped) { // a degenerate sliver: drop the flattest corner so the loop always ends
let best = 0, bv = Infinity;
for (let k = 0; k < idx.length; k++) { const v = Math.abs(cross(idx[(k + idx.length - 1) % idx.length], idx[k], idx[(k + 1) % idx.length])); if (v < bv) { bv = v; best = k; } }
idx.splice(best, 1);
}
}
if (idx.length === 3 && cross(idx[0], idx[1], idx[2]) > 1e-12) out.push(idx[0], idx[1], idx[2]);
return out;
}
In detail
Method
Shoelace area and centroid; ground sampled at cell centers inside the footprint; volume = Σ |z₀ − z| × (footprint area ÷ samples); ear-clipping triangulation.
Number types
64-bit numbers.
Tolerances
A mass with an area under 1e-9 is refused. Centroid falls back to the mean of the corners when |A| < 1e-12. Ear clipping uses 1e-12 on cross products. All absolute.
Limits
The mean ground and the base volumes depend on the 40 × 40 sampling. Samples where the surface has no elevation still take a share of the area but add no volume. The shoelace sums use absolute project coordinates.
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.