Resources Numerical accuracy Calculation policy

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.

platform.js CivilCraft 0.8.1 64-bit 8 calculations Reviewed

Tests bin/test-civilcraft-platform.mjs

Depth of cut and fill by cell

64-bit

The depth at every cell, the cut and fill volumes, and the deepest cut and highest fill.

dz = design − ground at ((i + 0.5)·c, (j + 0.5)·c); fill = Σ dz·c² (dz > 0); cut = Σ −dz·c² (dz ≤ 0)

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.

z = z₀ + grade × ((x − x₀)·cos θ + (y − y₀)·sin θ)

The reference point is subtracted first, so the projection works on small numbers.

  • platform.js
  • lines 103 to 106
  • CivilCraft 0.8.1
export function planePad(pts, { at, z, dir, grade }) {
  const ux = Math.cos(dir), uy = Math.sin(dir);
  return pts.map((p) => ({ ...p, z: z + grade * ((p.x - at.x) * ux + (p.y - at.y) * uy) }));
}

Ramp strip

64-bit

The four corners of a ramp between two points, its length and its grade.

L = hypot(Δx, Δy); grade = (z_b − z_a) ÷ L; corners = ends ± normal × width ÷ 2

A zero length or width returns no ramp instead of dividing.

  • platform.js
  • lines 108 to 113
  • CivilCraft 0.8.1
export function rampPad(a, b, za, zb, width) {
  const L = Math.hypot(b.x - a.x, b.y - a.y);
  if (!(L > 0) || !(width > 0)) return { pts: [], length: L, grade: 0, ok: false };
  const ux = (b.x - a.x) / L, uy = (b.y - a.y) / L, nx = -uy * width / 2, ny = ux * width / 2;
  return { pts: [{ x: a.x + nx, y: a.y + ny, z: za }, { x: b.x + nx, y: b.y + ny, z: zb }, { x: b.x - nx, y: b.y - ny, z: zb }, { x: a.x - nx, y: a.y - ny, z: za }], length: L, grade: (zb - za) / L, ok: true };
}

Free section

64-bit

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.

  • platform.js
  • lines 128 to 140
  • CivilCraft 0.8.1
export function freeSection(ground, design, line, { regions = null } = {}) {
  const length = lineLength(line);
  const g = cutSection(ground, line, { origin: 0 }), runsG = g.runs.map((r) => r.map((p) => ({ o: p.o, z: p.z })));
  let runsD = [];
  if (design) {
    runsD = cutSection(design, line, { origin: 0 }).runs.map((r) => r.map((p) => ({ o: p.o, z: p.z })));
    if (regions) runsD = clipRuns(runsD, polylineSpans(line, regions));
  }
  const cf = design ? cutFill(runsG, runsD) : { cut: 0, fill: 0, parts: [] };
  let acc = 0;
  const marks = line.map((p, i) => { if (i) acc += Math.hypot(p.x - line[i - 1].x, p.y - line[i - 1].y); return { o: acc, x: p.x, y: p.y }; });
  return { length, ground: runsG, design: runsD, cut: cf.cut, fill: cf.fill, parts: cf.parts, marks };
}

Stretches of a line inside the graded regions

64-bit

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.

Every policy that takes this approach