Resources Numerical accuracy Calculation policy

Mass-haul diagram

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.

masshaul.js CivilCraft 0.8.1 64-bit 5 calculations Reviewed

Tests bin/test-civilcraft-masshaul.mjs

Mass ordinate

64-bit

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.

volume = |peak| − |end level|; haul = ∫ (|m| − |end level|)⁺ d(sta) ÷ volume

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.

  • masshaul.js
  • lines 61 to 62
  • CivilCraft 0.8.1
const end = m;
return { points, balance, spans, totals: { cut, cutAdj: cut * k, fill, end, kind: Math.abs(end) < 1e-6 * Math.max(1, cut + fill) ? 'balanced' : end > 0 ? 'waste' : 'borrow' }, factor: k };

CSV export

64-bit

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.

Every policy that takes this approach