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.
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.
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.
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.
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.
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 };
}
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.
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.
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.
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.
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.