Cut and fill map, spot levels, sloped pads and free sections
Works out the depth of cut or fill cell by cell with its volumes, spot levels on a grid, the elevations of a sloped pad or ramp, and a section cut along any line with its cut and fill areas.
How the precision is kept
The map is the grid method: both surfaces are sampled at cell centers on a grid anchored at whole multiples of the cell size, in 64-bit. The cell size sets the accuracy. Sloped pads are a closed-form plane. The section CSV rounds every value to 2 decimals.
Calculations under this policy
8 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.
Cell centers come from whole-number indexes on a grid anchored at multiples of the cell size. Cut and fill are running sums of same-sign terms with no compensation. The automatic cell size is not rounded.
platform.js
lines 32 to 60
CivilCraft 0.8.1
export function depthMap(ground, design, { cell, region = null, bounds = null, maxCells = 4e6 } = {}) {
const ba = ground.stats().bounds, bb = design.stats().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?.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;
const out = { cell: cell || 0, cells: [], cut: 0, fill: 0, maxCut: 0, maxFill: 0, minDz: 0, maxDz: 0 };
if (!(W > 0 && H > 0)) return out;
let c = cell > 0 ? cell : Math.sqrt((W * H) / 40000);
while ((W / c) * (H / c) > maxCells) c *= 2;
out.cell = c;
const inPoly = (poly, x, y) => { let k = false; for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) { const p = poly[i], q = poly[j]; if ((p.y > y) !== (q.y > y) && x < ((q.x - p.x) * (y - p.y)) / (q.y - p.y) + p.x) k = !k; } return k; };
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), A = c * c;
for (let j = j0; j < j1; j++) for (let i = i0; i < i1; i++) {
const x = (i + 0.5) * c, y = (j + 0.5) * c;
if (regions && !regions.some((poly) => inPoly(poly, x, y))) continue;
const z0 = ground.elevationAt(x, y), z1 = design.elevationAt(x, y);
if (z0 === null || z1 === null) continue;
const dz = z1 - z0;
out.cells.push({ x, y, dz });
if (dz > 0) { out.fill += dz * A; if (dz > out.maxFill) out.maxFill = dz; } else { out.cut -= dz * A; if (-dz > out.maxCut) out.maxCut = -dz; }
if (dz < out.minDz) out.minDz = dz; if (dz > out.maxDz) out.maxDz = dz;
}
return out;
}
Depth classes
64-bit
Equal steps of depth for shading the map, with a level band around zero.
n = ⌈depth ÷ step − 1e-9⌉; level when |dz| < step ÷ 20
The 1e-9 keeps a depth that is an exact multiple of the step in the lower class. The automatic step is a 1-2-5 value near a fifth of the greatest depth.
platform.js
lines 64 to 84
CivilCraft 0.8.1
export function depthClasses(map, { step = 0 } = {}) {
const span = Math.max(map.maxCut, map.maxFill);
const s = step > 0 ? step : niceStep(span / 5 || 1);
const nCut = Math.max(1, Math.ceil(map.maxCut / s - 1e-9)), nFill = Math.max(1, Math.ceil(map.maxFill / s - 1e-9));
const levels = [];
for (let k = nCut; k >= 1; k--) levels.push(-k * s);
levels.push(0);
for (let k = 1; k <= nFill; k++) levels.push(k * s);
// class index: 0 … nCut-1 cut (deepest first), nCut = level (|dz| < s/20), nCut+1 … fill (shallowest first)
const eps = s / 20;
const classOf = (dz) => {
if (Math.abs(dz) < eps) return nCut;
if (dz < 0) return Math.max(0, nCut - Math.ceil(-dz / s - 1e-9));
return nCut + Math.min(nFill, Math.ceil(dz / s - 1e-9));
};
const legend = [];
for (let k = nCut; k >= 1; k--) legend.push({ lo: -k * s, hi: -(k - 1) * s, kind: 'cut' });
legend.push({ lo: -eps, hi: eps, kind: 'level' });
for (let k = 1; k <= nFill; k++) legend.push({ lo: (k - 1) * s, hi: k * s, kind: 'fill' });
return { step: s, levels, classOf, legend, nCut, nFill };
}
Spot levels
64-bit
Ground level, design level and their difference on a grid, with the deepest cut and highest fill marked.
The points are the depth map's cell centers at the chosen spacing; the two levels are read again from the surfaces there. A difference below 0.001 of the spacing is called level.
platform.js
lines 86 to 98
CivilCraft 0.8.1
export function spotLevels(ground, design, { spacing, region = null, bounds = null } = {}) {
if (!(spacing > 0)) return [];
const m = depthMap(ground, design, { cell: spacing, region, bounds });
const spots = m.cells.map((c) => {
const zg = ground.elevationAt(c.x, c.y), zd = design.elevationAt(c.x, c.y);
return { x: c.x, y: c.y, zg, zd, dz: c.dz, kind: Math.abs(c.dz) < spacing * 1e-3 ? 'level' : c.dz < 0 ? 'cut' : 'fill', extreme: false };
});
let lo = null, hi = null;
for (const s of spots) { if (!lo || s.dz < lo.dz) lo = s; if (!hi || s.dz > hi.dz) hi = s; }
if (lo && lo.dz < 0) lo.extreme = true;
if (hi && hi.dz > 0) hi.extreme = true;
return spots;
}
Sloped pad as a plane
64-bit
The elevation of each pad corner on a plane through a point, rising in a direction at a grade.
Ground and design cut along a polyline, with cut and fill areas and the distance of each vertex.
The cut is made by cutSection (terrain-section.js) and the areas by cutFill (sections.js); this function passes distances and elevations through unchanged and adds the vertex distances as a running sum of segment lengths.
The distances along the section line where it runs inside a graded region.
t = ((p − a) × e) ÷ (d × e); inside tested at the middle of each piece
Crossings with region edges are parametric solves; edges with |d × e| below 1e-14 (absolute, length squared) are skipped as parallel. Each piece between crossings is kept when its middle is inside a region by crossing count.
platform.js
lines 142 to 167
CivilCraft 0.8.1
/** Distances along a polyline where it is inside any of the polygons (or on their outline). */
export function polylineSpans(line, regions) {
const out = [];
const inside = (x, y) => regions.some((r) => { let c = false; for (let i = 0, j = r.length - 1; i < r.length; j = i++) if ((r[i].y > y) !== (r[j].y > y) && x < ((r[j].x - r[i].x) * (y - r[i].y)) / (r[j].y - r[i].y) + r[i].x) c = !c; return c; });
let acc = 0;
for (let k = 1; k < line.length; k++) {
const a = line[k - 1], b = line[k], dx = b.x - a.x, dy = b.y - a.y, L = Math.hypot(dx, dy);
if (!(L > 0)) continue;
const ts = new Set([0, 1]);
for (const r of regions) for (let i = 0; i < r.length; i++) {
const p = r[i], q = r[(i + 1) % r.length], ex = q.x - p.x, ey = q.y - p.y, d = dx * ey - dy * ex;
if (Math.abs(d) < 1e-14) continue;
const tt = ((p.x - a.x) * ey - (p.y - a.y) * ex) / d, u = ((p.x - a.x) * dy - (p.y - a.y) * dx) / d;
if (tt > 0 && tt < 1 && u >= 0 && u <= 1) ts.add(tt);
}
const xs = [...ts].sort((m, n) => m - n);
for (let i = 1; i < xs.length; i++) {
const tm = (xs[i - 1] + xs[i]) / 2;
if (xs[i] - xs[i - 1] < 1e-12 || !inside(a.x + dx * tm, a.y + dy * tm)) continue;
const o0 = acc + L * xs[i - 1], o1 = acc + L * xs[i], last = out[out.length - 1];
if (last && Math.abs(last[1] - o0) < 1e-9) last[1] = o1; else out.push([o0, o1]);
}
acc += L;
}
return out;
}
Section CSV
64-bit
Writes the section as distance, ground level and design level rows, then the cut and fill areas.
value = round(v × 100) ÷ 100
Every distance, level and area is rounded to 2 decimals before it is written, so the file holds rounded values. Levels at a distance present in only one of the two lines are linear interpolations.
platform.js
lines 169 to 178
CivilCraft 0.8.1
const f2 = (v) => (Number.isFinite(v) ? (Math.round(v * 100) / 100).toString() : '');
export function freeSectionCsv(sec, { units = 'ft', t = (k, v) => k } = {}) {
const rows = [[t('sx.su914.csv.dist', { unit: units }), t('sx.su914.csv.ground'), t('sx.su914.csv.design')]];
const os = new Set();
for (const r of [...sec.ground, ...sec.design]) for (const p of r) os.add(p.o);
const zAt = (runs, o) => { for (const r of runs) { if (!r.length || o < r[0].o - 1e-9 || o > r[r.length - 1].o + 1e-9) continue; for (let i = 1; i < r.length; i++) if (o <= r[i].o + 1e-9) { const w = r[i].o - r[i - 1].o; return w > 0 ? r[i - 1].z + (r[i].z - r[i - 1].z) * (o - r[i - 1].o) / w : r[i].z; } return r[0].z; } return null; };
for (const o of [...os].sort((a, b) => a - b)) { const g = zAt(sec.ground, o), d = zAt(sec.design, o); rows.push([f2(o), g === null ? '' : f2(g), d === null ? '' : f2(d)]); }
rows.push([], [t('sx.su914.csv.cut', { unit: units + '²' }), f2(sec.cut)], [t('sx.su914.csv.fill', { unit: units + '²' }), f2(sec.fill)]);
return rows.map((r) => r.join(',')).join('\r\n') + '\r\n';
In detail
Method
Grid method, volume = Σ |design − ground| × cell²; plane z = z₀ + grade × distance along the rise direction; section cut by terrain-section.js, areas by sections.js.
Number types
64-bit numbers.
Tolerances
Default cell = √(width × height ÷ 40000), doubled while over 4,000,000 cells. Level band: |dz| < step ÷ 20 in the classes, |dz| < spacing × 0.001 for spot levels. Class boundaries allow 1e-9 of a step. Region crossings: parallel when |d| < 1e-14 (absolute); spans shorter than 1e-12 of a segment ignored; spans joined within 1e-9.
Limits
A cell is counted whole or not at all by its center. The exact section cut and its areas are computed in terrain-section.js and sections.js. CSV values carry 2 decimals.
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.