Works out the mass ordinate at each section, the balance points, the volume moved in each balanced stretch and its average haul distance.
How the precision is kept
The mass ordinate is a 64-bit running sum of cut × factor − fill from the first section. It is taken as straight between sections, so balance points and haul distances follow the section spacing. The CSV export rounds volumes to 1 decimal and haul distances to 2.
Calculations under this policy
5 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 running total of earth along the alignment, and the cut and fill totals.
m_i = m_(i−1) + cut_i × factor − fill_i
A plain running sum in section order with no compensation; terms of both signs are added, so the ordinate near a balance point is a small difference of large totals.
masshaul.js
lines 26 to 28
CivilCraft 0.8.1
rows.forEach((r, i) => {
if (i > 0) { m += (r.cutVol || 0) * k - (r.fillVol || 0); cut += r.cutVol || 0; fill += r.fillVol || 0; }
points.push({ sta: r.sta, m });
Balance points
64-bit
The stations where the mass ordinate crosses zero.
sta = sta_a + (sta_b − sta_a) × m_a ÷ (m_a − m_b)
The two ordinates have opposite signs, so m_a − m_b adds their sizes and does not cancel. An ordinate of exactly 0 at a section is taken as a balance point there.
masshaul.js
lines 31 to 36
CivilCraft 0.8.1
const balance = [];
for (let i = 1; i < points.length; i++) {
const a = points[i - 1], b = points[i];
if ((a.m > 0 && b.m < 0) || (a.m < 0 && b.m > 0)) balance.push(a.sta + ((b.sta - a.sta) * a.m) / (a.m - b.m));
else if (b.m === 0 && a.m !== 0 && i < points.length - 1) balance.push(b.sta);
}
Volume moved and average haul
64-bit
For each balanced stretch, the volume moved and the average distance it travels.
The area is a sum of trapezoids, with a triangle where a piece crosses the balance line (the crossing found by linear interpolation). A stretch whose volume is 1e-9 or less is skipped, which also guards the division.
masshaul.js
lines 47 to 59
CivilCraft 0.8.1
const e = pts.at(-1).m;
let peak = 0;
for (const q of pts) if (Math.abs(q.m) > Math.abs(peak)) peak = q.m;
const volume = Math.abs(peak) - Math.abs(e);
if (!(volume > 1e-9)) continue;
const sg = Math.sign(peak), ex = (v) => Math.max(0, sg * v - Math.abs(e)); // how far past the balance line
let area = 0;
for (let i = 1; i < pts.length; i++) {
const a = pts[i - 1], b = pts[i], fa = ex(a.m), fb = ex(b.m), L = b.sta - a.sta;
if (fa > 0 && fb > 0) area += ((fa + fb) / 2) * L;
else if (fa > 0 || fb > 0) { const ga = sg * a.m - Math.abs(e), gb = sg * b.m - Math.abs(e), x = (ga / (ga - gb)) * L; area += fa > 0 ? (fa * x) / 2 : (fb * (L - x)) / 2; } // crosses the line
}
spans.push({ from: s0, to: s1, volume, dir: peak > 0 ? 'ahead' : 'back', haul: area / volume });
Totals and what is left over
64-bit
Total cut, adjusted cut, fill, the end ordinate, and whether the job wastes, borrows or balances.
balanced when |end| < 1e-6 × max(1, cut + fill)
The balanced test is relative to the total volume handled, with a floor of 1. Adjusted cut is one multiplication of the cut total by the factor.
Writes the stations, ordinates and spans as a CSV file in cubic yards or cubic meters.
cubic yards = cubic feet ÷ 27
Volumes are divided by the exact factor 27 (feet) or 1 (meters) and written with 1 decimal; haul distances with 2 decimals. The file therefore holds rounded values, not the 64-bit results.
masshaul.js
lines 65 to 73
CivilCraft 0.8.1
export function massHaulCsv(res, { units = 'ft', station = (s) => String(s) } = {}) {
const u = unitOf(units), v = (x) => fixed(x / u.volPer, 1).replace(/,/g, ''), d = (x) => fixed(x, 2).replace(/,/g, '');
const rows = [[t('mh.c.sta'), t('mh.c.m', { u: u.vol })]];
for (const p of res.points) rows.push([station(p.sta), v(p.m)]);
rows.push([], [t('mh.c.from'), t('mh.c.to'), t('mh.c.vol', { u: u.vol }), t('mh.c.dir'), t('mh.c.haul', { u: units === 'm' ? 'm' : 'ft' })]);
for (const s of res.spans) rows.push([station(s.from), station(s.to), v(s.volume), t('mh.dir.' + s.dir), d(s.haul)]);
rows.push([], [t('mh.c.factor'), String(res.factor)], [t('mh.c.end', { u: u.vol }), v(res.totals.end)]);
return rows.map((r) => r.map((x) => (/[",\r\n]/.test(x) ? '"' + x.replace(/"/g, '""') + '"' : x)).join(',')).join('\r\n') + '\r\n';
}
In detail
Method
Mass ordinate Σ (cut × factor − fill); balance points by linear interpolation at zero crossings; haul = area between the curve and the balance line ÷ volume moved.
Number types
64-bit numbers.
Tolerances
A stretch with less than 1e-9 volume moved is left out (absolute). Station comparisons use 1e-9. The end is balanced when |end| < 1e-6 × max(1, cut + fill).
Limits
The input volumes come from sections.js (average end areas). The diagram is piecewise linear between sections.
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.