Resources Numerical accuracy Calculation policy

Earthwork, route and pond tools: values stored and exported

Carries the results of the earthwork balancer, the route finder and the pond tools into the project, into CSV files and into the answers given to assistants.

How the precision is kept

The calculations behind these tools are 64-bit, but the values are rounded when they leave: pad levels and corner elevations to 3 decimals, curve radii to 2 or 3 decimals, an orifice diameter to 3 decimals, CSV columns to 1 to 4 decimals, and every number returned to an assistant to 3 decimals. One unit factor is written to 9 digits.

Open item. Store pad levels, corner elevations and curve radii unrounded, and replace 39.3700787 by 1 ÷ 0.0254.

Calculations under this policy

14 calculations in 2 source files. 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.

eg-ui.js CivilCraft 0.8.1 64-bit 8 calculations Reviewed

Tests None found by the review

Earthwork balancer run

64-bit

The volumes at the pad's present level, the balanced level, the tilt and the haul plan for one pad.

factor = 1 + percent ÷ 100; cell area = cell²

The cell area is the square of the sampling cell and is passed to every volume. The swell or shrink percentage becomes a factor by one division and one addition. Nothing is rounded here.

  • eg-ui.js
  • lines 58 to 66
  • CivilCraft 0.8.1
const pad = pads.find((p) => p.id === S.pad), factor = 1 + S.factor / 100;
const cells = cellsInside(ground, pad.pts), A = (cells.cell || 1) ** 2;
if (!cells.length) { status.textContent = t('eg.ew.nocells'); return; }
const now = volumes(cells, planeOf({}), Number(pad.z), { area: A, factor });
const flat = balanceLevel(cells, planeOf({}), { area: A, factor });
const plane = S.tilt > 0 || S.fall > 0 ? bestPlane(cells, { area: A, factor, maxGrade: Math.max(S.tilt, S.fall) / 100, minGrade: S.fall / 100 }) : null;
const pick = plane || { ...flat, gx: 0, gy: 0, cx: 0, cy: 0 };
const design = planeOf({ cx: pick.cx, cy: pick.cy, gx: pick.gx, gy: pick.gy });
const hp = haulPlan(cells, design, pick.z0, { area: A, factor, blocks: S.blocks });

Balanced level written to the pad

64-bit

The pad's level, and its corner elevations when it tilts, as stored in the project.

z stored = Number(z.toFixed(3))

The balanced level is rounded to 3 decimals before it is stored, and so is each corner elevation of a tilted pad. The stored pad therefore differs from the balanced one by up to 0.0005 units of level.

  • eg-ui.js
  • lines 75 to 77
  • CivilCraft 0.8.1
const apply = btn(t('eg.ew.apply'), 'check2-circle', () => {
  P.change(t('eg.ew.undo'), (st) => { const q = st.pads.find((x) => x.id === pad.id); if (!q) return; q.z = Number(fixed(pick.z0, 3)); if (plane) q.pts = q.pts.map((c) => ({ x: c.x, y: c.y, z: Number(fixed(design(c.x, c.y, pick.z0), 3)) })); else q.pts = q.pts.map((c) => ({ x: c.x, y: c.y })); });
  ctx.say(t('eg.ew.applied', { z: fixed(pick.z0, 2) })); ctx.refresh(); run();

Haul plan CSV

64-bit

The haul flows exported as a CSV file.

Coordinates and distances are written with 2 decimals and volumes with 3, by toFixed. The file holds the rounded text only.

  • eg-ui.js
  • line 80
  • CivilCraft 0.8.1
el('div', { className: 'cv-actions' }, apply, btn('CSV', 'download', () => dl(stem('earthwork') + '.csv', csv(['from_x', 'from_y', 'to_x', 'to_y', 'volume_' + uo().vol, 'distance'], hp.flows.map((f) => [fixed(f.from.x, 2), fixed(f.from.y, 2), fixed(f.to.x, 2), fixed(f.to.y, 2), fixed(f.vol / uo().volPer, 3), fixed(f.dist, 2)]))), 'data-eg-ew-csv')));   // i18n-ignore: file type, csv headers

Alignment from a found route

64-bit

The PIs of a new alignment from the route's points, with a curve radius at each bend.

r = min(R, min(l₁, l₂) ÷ (2·tan(Δ/2))), stored to 2 decimals

The deflection is a difference of two atan2 directions folded into [0, π]. The radius is capped so its tangent fits half the shorter leg, then rounded to 2 decimals before it is stored. PI coordinates are copied unrounded.

  • eg-ui.js
  • lines 125 to 138
  • CivilCraft 0.8.1
function makeAlignment(r) {
  const P = ctx.project(), id = P.nextId('a'), n = P.state.alignments.length + 1, R = S.radius;
  const pis = r.path.map((q, i, a) => {
    let rad = 0;
    if (i > 0 && i < a.length - 1 && R > 0) {
      const l1 = Math.hypot(q.x - a[i - 1].x, q.y - a[i - 1].y), l2 = Math.hypot(a[i + 1].x - q.x, a[i + 1].y - q.y), t1 = Math.atan2(q.y - a[i - 1].y, q.x - a[i - 1].x), t2 = Math.atan2(a[i + 1].y - q.y, a[i + 1].x - q.x);
      let d = Math.abs(t2 - t1); if (d > Math.PI) d = 2 * Math.PI - d;
      rad = d > 1e-6 ? Math.min(R, Math.min(l1, l2) / (2 * Math.tan(d / 2))) : 0;
    }
    return { x: q.x, y: q.y, r: Number(fixed(rad, 2)) };
  });
  P.change(t('eg.rt.undo'), (st) => st.alignments.push({ id, name: t('eg.rt.name', { n }), start: 0, pis, pvis: [] }));
  ctx.say(t('eg.rt.made')); ctx.refresh();
}

Route ends picked in the plan

64-bit

The start and end of a route search as taken from two picked points.

x, y = Number(v.toFixed(3))

Picked coordinates are rounded to 3 decimals before they are used as the ends of the search. The search itself snaps to a grid cell, which is much coarser.

  • eg-ui.js
  • lines 470 to 471
  • CivilCraft 0.8.1
routeState.fx = Number(fixed(pts[0].x, 3)); routeState.fy = Number(fixed(pts[0].y, 3)); routeState.tx = Number(fixed(pts[pts.length - 1].x, 3)); routeState.ty = Number(fixed(pts[pts.length - 1].y, 3));
route(true); return true;

Sized orifice written to the pond

64-bit

The orifice diameter found for the target outflow, as stored in the pond.

diameter stored = Number((d ÷ L).toFixed(3))

The diameter from the bisection is converted to project units and rounded to 3 decimals before it is stored. The stored orifice can therefore pass slightly more or less than the target that the unrounded one met.

  • eg-ui.js
  • lines 350 to 353
  • CivilCraft 0.8.1
const L = r0.conv.len, Q = r0.conv.flow, z0 = (D.start ?? D.storage.bottom) * L;
const s = sizeOrifice(r0.hydro.q, r0.hydro.dt, r0.rows, { target: D.target * Q, z0, base: D.storage.bottom * L, devices: r0.devices });
if (!s.met) { ctx.say(t('eg.pd.size.no')); return; }
D.devices = [{ kind: 'orifice', invert: D.storage.bottom, diameter: Number(fixed(s.diameter / L, 3)), cd: 0.6 }, ...D.devices.filter((d) => d.kind !== 'orifice')];

Pond hydrograph CSV

64-bit

Inflow, outflow and stage against time exported as a CSV file.

Flows are written with 4 decimals and stage with 3, in project units. The file holds the rounded text only.

  • eg-ui.js
  • line 368
  • CivilCraft 0.8.1
el('div', { className: 'cv-actions' }, btn('CSV', 'download', () => dl(stem('pond') + '.csv', csv(['minutes', 'inflow_' + fu, 'outflow_' + fu, 'stage_' + u], r.route.q.map((q, i) => [i * r.hydro.dt, fixed((r.hydro.q[i] || 0) / r.conv.flow, 4), fixed(q / r.conv.flow, 4), fixed(r.route.z[i] / r.conv.len, 3)]))), 'data-eg-pd-csv')));   // i18n-ignore: file type, csv headers

Survey points for progress checks

64-bit

Ground points read from a LAS, LAZ, CSV or text file and turned into a surface.

LAS thinning cell = max(0.5, √(extent area ÷ 400 000)) unless a spacing is given

Text files: the last three numbers on each line are taken as x, y, z and converted with Number, unrounded. LAS files are thinned to the lowest point per cell before the surface is built, so the surface is known to that cell size.

  • eg-ui.js
  • lines 441 to 462
  • CivilCraft 0.8.1
async function loadSurvey(file, S) {
  const { buildGround } = await import('./ground-surface.js');
  const { Thinner } = await import('./thin.js');
  let pts = [];
  const spacing = Number(S.spacing) > 0 ? Number(S.spacing) : 0;
  if (/\.la[sz]$/i.test(file.name)) {
    const { readLasHeader, scanPoints } = await import('./las.js');
    const h = await readLasHeader(file), cell = spacing || Math.max(0.5, Math.sqrt(((h.max?.[0] - h.min?.[0]) * (h.max?.[1] - h.min?.[1])) / 400000) || 1);
    const th = new Thinner({ cell, mode: 'lowest', max: 2e6 });
    await scanPoints(file, h, (x, y, z, c) => { if (c !== 7 && c !== 18) th.add(x, y, z, c); });
    pts = th.points();
    if (!pts.some((p) => p.cls === 2)) { const { classifyGround } = await import('./ground-classify.js'); pts = classifyGround(pts, { cell, maxObject: cell * 20, slope: 0.3 }).ground; }
    else pts = pts.filter((p) => p.cls === 2);
  } else {
    const text = await file.text();
    for (const line of text.split(/\r?\n/)) { const m = line.trim().split(/[\s,;]+/).map(Number); if (m.length >= 3 && m.slice(-3).every(Number.isFinite)) { const [x, y, z] = m.slice(-3); pts.push({ x, y, z }); } if (pts.length > 2e6) break; }
  }
  if (pts.length < 3) throw new Error(t('eg.pg.few'));
  const r = buildGround({ points: pts, spacing: 0 });
  if (!r.surface) throw new Error(t('eg.pg.nosurface'));
  return { surface: r.surface, n: pts.length };
}
eg-agent.js CivilCraft 0.8.1 64-bit 6 calculations Reviewed

Tests bin/test-civilcraft-eg.mjs

Three-decimal rounding of tool results

64-bit

The rounding applied to nearly every number a tool returns to an assistant or script.

r3(v) = round(v × 1000) ÷ 1000

Rounds to 3 decimals whatever the size or unit of the value. A level in feet keeps 0.001 ft; a small ratio such as a share of the site keeps only three decimals of the ratio.

  • eg-agent.js
  • line 31
  • CivilCraft 0.8.1
const r3 = (v) => (Number.isFinite(v) ? Math.round(v * 1000) / 1000 : null);

Volumes in report units

64-bit

Volumes converted to cubic yards or cubic meters for a tool's answer.

cubic yards = cubic feet ÷ 27; from cubic meters: ÷ 0.028316846592 ÷ 27

0.028316846592 is the exact cubic meter value of a cubic international foot (0.3048³) and 27 is exact. The result is then rounded to 3 decimals.

  • eg-agent.js
  • lines 44 to 45
  • CivilCraft 0.8.1
const vol = (v) => r3(v / uo().per);   // volumes worked out in project units (cells are in project units)
const volSI = (v) => r3(M() ? v : v / 0.028316846592 / 27);   // volumes that come from the rain solver are cubic metres

Rain results in inches and cubic feet per second

64-bit

Depths and flows from the rain solver converted from meters for a feet project.

inches = meters × 39.3700787; ft³/s = m³/s ÷ 0.028316846592

The inches factor is written to 9 significant digits; the exact value is 1 ÷ 0.0254 = 39.37007874015748, so converted depths are off by about 1 part in a billion before the 3-decimal rounding. The ponding threshold is converted to meters with the same rounded factor.

  • eg-agent.js
  • lines 67 to 68
  • CivilCraft 0.8.1
const res = sim.run(), flow = (q) => (M() ? q : q / 0.028316846592), dU = M() ? 'mm' : 'in', dOf = (m) => (M() ? m * 1000 : m * 39.3700787);
const thr = (a.pondedDepth ?? (M() ? 50 : 2)) / (M() ? 1000 : 39.3700787);

Pad level set by an assistant

64-bit

A pad's level and, for a tilted pad, its corner elevations, as stored in the project.

z stored = r3(level); corner z = r3(level + gx·(x − cx) + gy·(y − cy))

The level and every corner elevation are rounded to 3 decimals before they are stored. The default center is the plain mean of the corner coordinates, not the area centroid.

  • eg-agent.js
  • lines 105 to 106
  • CivilCraft 0.8.1
const cx = a.centre?.x ?? pad.pts.reduce((s, q) => s + q.x, 0) / pad.pts.length, cy = a.centre?.y ?? pad.pts.reduce((s, q) => s + q.y, 0) / pad.pts.length, d = planeOf({ cx, cy, gx: a.gradeX ?? 0, gy: a.gradeY ?? 0 });
P().change('Assistant: set a pad level', (st) => { const q = st.pads.find((x) => x.id === a.pad); q.z = r3(a.level); q.pts = q.pts.map((c) => (a.gradeX || a.gradeY ? { x: c.x, y: c.y, z: r3(d(c.x, c.y, a.level)) } : { x: c.x, y: c.y })); });

Alignment from route points

64-bit

The PIs of an alignment made by an assistant from route points, with curve radii.

r = min(R, min(l₁, l₂) ÷ (2·tan(Δ/2))), stored to 3 decimals

The same radius rule as the window's button, but rounded to 3 decimals here and to 2 there, so the two paths can store different radii for the same route.

  • eg-agent.js
  • lines 133 to 140
  • CivilCraft 0.8.1
const pis = pts.map((q, i) => {
  let r = 0;
  if (i > 0 && i < pts.length - 1 && R > 0) {
    const l1 = Math.hypot(q.x - pts[i - 1].x, q.y - pts[i - 1].y), l2 = Math.hypot(pts[i + 1].x - q.x, pts[i + 1].y - q.y);
    let d = Math.abs(Math.atan2(pts[i + 1].y - q.y, pts[i + 1].x - q.x) - Math.atan2(q.y - pts[i - 1].y, q.x - pts[i - 1].x)); if (d > Math.PI) d = 2 * Math.PI - d;
    r = d > 1e-6 ? Math.min(R, Math.min(l1, l2) / (2 * Math.tan(d / 2))) : 0;
  }
  return { x: q.x, y: q.y, r: r3(r) };

Orifice sized by an assistant

64-bit

The orifice diameter found for an allowed outflow, as used for the reported run.

diameter = r3(d ÷ L)

The diameter is rounded to 3 decimals in project units, and the pond is then routed again with the rounded diameter, so the reported peak belongs to the rounded orifice.

  • eg-agent.js
  • lines 159 to 161
  • CivilCraft 0.8.1
const s = sizeOrifice(r0.hydro.q, r0.hydro.dt, r0.rows, { target: a.sizeOrificeFor * r0.conv.flow, z0: (D.start ?? D.storage.bottom) * L, base: D.storage.bottom * L, devices: r0.devices });
if (!s.met) return { sized: false, reason: 'The pond is too small to hold the peak to that outflow.' };
D.devices = [{ kind: 'orifice', invert: D.storage.bottom, diameter: r3(s.diameter / L), cd: 0.6 }, ...D.devices.filter((d) => d.kind !== 'orifice')]; D.target = a.sizeOrificeFor;

In detail

Method
Results are converted to project units and rounded with toFixed or Math.round(v × 1000) ÷ 1000 at the point where they are stored, exported or returned.
Number types
64-bit numbers; rounding to decimal places as listed.
Tolerances
Curve radius at a bend only when the deflection is above 1e-6 rad. Haul plan lines for spare or short volume shown above 1e-6.
Limits
A value rounded to 3 decimals of a foot or meter is good to 0.0005 of that unit. Inches per meter is written as 39.3700787; the exact value is 1 ÷ 0.0254 = 39.37007874015748.

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