Cut, fill and net volume
64-bitThe volumes reported between the existing and the proposed surface.
fill = Σ (z₁ − z₀)·c² where z₁ > z₀; cut = Σ (z₀ − z₁)·c² otherwise; net = fill − cut; area = cells × c²
Cell centers are (i + 0.5) × c from whole-number indexes, so positions carry no accumulated step error. Cut and fill are each a running sum of same-sign terms added in row order without compensation; net is their difference, which loses digits when cut and fill nearly balance.
export function gridVolume(base, top, { cell, region = null, bounds = null, maxCells = 4e6 } = {}) {
const ba = base.bounds, bb = top.bounds;
let b = bounds || { minX: Math.max(ba.minX, bb.minX), minY: Math.max(ba.minY, bb.minY), maxX: Math.min(ba.maxX, bb.maxX), maxY: Math.min(ba.maxY, bb.maxY) };
const regions = region ? (Array.isArray(region[0]) ? region : [region]).filter(Boolean) : null;
if (regions && regions.length) {
const r = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
for (const poly of regions) for (const p of poly) { r.minX = Math.min(r.minX, p.x); r.minY = Math.min(r.minY, p.y); r.maxX = Math.max(r.maxX, p.x); r.maxY = Math.max(r.maxY, p.y); }
b = { minX: Math.max(b.minX, r.minX), minY: Math.max(b.minY, r.minY), maxX: Math.min(b.maxX, r.maxX), maxY: Math.min(b.maxY, r.maxY) };
}
const W = b.maxX - b.minX, H = b.maxY - b.minY;
if (!(W > 0 && H > 0)) return { cut: 0, fill: 0, net: 0, cells: 0, cell: cell || 0, area: 0 };
let c = cell > 0 ? cell : niceCell(Math.sqrt((W * H) / 250000));
while ((W / c) * (H / c) > maxCells) c *= 2;
// cells on a grid anchored at whole multiples of c, so results do not depend on the bounds
const i0 = Math.floor(b.minX / c), i1 = Math.ceil(b.maxX / c), j0 = Math.floor(b.minY / c), j1 = Math.ceil(b.maxY / c);
let cutV = 0, fillV = 0, cells = 0;
const A = c * c;
for (let j = j0; j < j1; j++) {
const y = (j + 0.5) * c;
for (let i = i0; i < i1; i++) {
const x = (i + 0.5) * c;
if (regions && !regions.some((poly) => pointInPoly(x, y, poly))) continue;
const z0 = base.elevationAt(x, y), z1 = top.elevationAt(x, y);
if (z0 === null || z1 === null) continue;
cells++;
const dz = z1 - z0;
if (dz > 0) fillV += dz * A; else cutV -= dz * A;
}
}
return { cut: cutV, fill: fillV, net: fillV - cutV, cells, cell: c, area: cells * A };
}