Resources Numerical accuracy Calculation policy

Ground modifiers: cut and fill, overlaps, level labels and junction checks

Reports the cut and fill each landscape object makes in the ground, where two of them overlap, the levels and gradients read off the surface, and where walls, steps and planted areas do not meet the levels beside them.

How the precision is kept

64-bit numbers throughout. Cut and fill per object are summed on a square grid over the objects, one elevation difference per cell, so they are estimates whose accuracy follows the cell size. Overlap areas come from the exact polygon clip in areas.js. The junction checks compare levels against labeled typical thresholds of 10 mm and 150 mm, read at wall stations.

Calculations under this policy

8 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.

landscape/stack.js CivilCraft 0.8.1 64-bit 3 calculations Reviewed

Tests bin/test-landscape.mjs

Overlap area of two modifiers

64-bit

The plan area where two ground-shaping objects overlap.

overlap = |area of a| − area of (a with b cut out)

The overlap is the difference of two areas, which are nearly equal when the overlap is small, so a small overlap carries few digits. It is reported only above 1e-6 of the first area plus 1e-9.

  • landscape/stack.js
  • lines 38 to 49
  • CivilCraft 0.8.1
export function overlapPairs(regions) {
  const out = [];
  const R = regions.filter((r) => r && r.pts?.length >= 3);
  for (let i = 0; i < R.length; i++) for (let j = i + 1; j < R.length; j++) {
    const a = R[i], b = R[j];
    const full = Math.abs(polyArea(a.pts));
    if (!(full > 0)) continue;
    const left = clipRegion(a.pts, [b.pts]).area, area = full - left;
    if (area > full * 1e-6 + 1e-9) out.push({ a: a.id, b: b.id, area });
  }
  return out;
}

Cut and fill per modifier

64-bit

The cut volume, fill volume and area that each ground-shaping object accounts for.

cell = √(W × H ÷ 200000); fill += (z proposed − z existing) × cell² when positive, cut when negative

A grid estimate: one elevation difference at each cell center, with cell centers stepped by repeated addition. A cell counts whole or not at all, so the volume near an outline is good to about one cell. Sums run in grid order.

  • landscape/stack.js
  • lines 51 to 74
  • CivilCraft 0.8.1
export function cutFillBy(existing, proposed, regions, { cell = 0, maxCells = 2e6 } = {}) {
  const R = regions.filter((r) => r && r.pts?.length >= 3);
  const by = {}, total = { cut: 0, fill: 0, area: 0 };
  for (const r of R) by[r.id] = { cut: 0, fill: 0, area: 0 };
  if (!R.length || !existing || !proposed) return { by, total, cell: 0 };
  let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
  const boxes = R.map((r) => { let a = Infinity, b = Infinity, c = -Infinity, d = -Infinity; for (const p of r.pts) { a = Math.min(a, p.x); b = Math.min(b, p.y); c = Math.max(c, p.x); d = Math.max(d, p.y); } x0 = Math.min(x0, a); y0 = Math.min(y0, b); x1 = Math.max(x1, c); y1 = Math.max(y1, d); return [a, b, c, d]; });
  const W = x1 - x0, H = y1 - y0;
  if (!(W > 0 && H > 0)) return { by, total, cell: 0 };
  let c = cell > 0 ? cell : Math.sqrt((W * H) / 200000);
  while ((W / c) * (H / c) > maxCells) c *= 2;
  const A = c * c;
  for (let y = y0 + c / 2; y < y1; y += c) for (let x = x0 + c / 2; x < x1; x += c) {
    let k = -1;
    for (let i = R.length - 1; i >= 0; i--) { const b = boxes[i]; if (x >= b[0] && x <= b[2] && y >= b[1] && y <= b[3] && pointIn(x, y, R[i].pts)) { k = i; break; } }
    if (k < 0) continue;
    const z0 = existing.elevationAt(x, y), z1 = proposed.elevationAt(x, y);
    if (!fin(z0) || !fin(z1)) continue;
    const dz = z1 - z0, o = by[R[k].id];
    o.area += A; total.area += A;
    if (dz > 0) { o.fill += dz * A; total.fill += dz * A; } else { o.cut -= dz * A; total.cut -= dz * A; }
  }
  return { by, total, cell: c };
}

Spot level and gradient

64-bit

The level at a point, or the fall, run, percent and ratio between two points on the surface.

pct = |z₂ − z₁| ÷ run × 100; ratio = run ÷ |z₂ − z₁|

The run is the plan distance from Math.hypot; two points 1e-9 or less apart are refused earlier, so the division is guarded. A gradient of 1e-9 or less gives a ratio of Infinity. The label text is rounded separately when drawn.

  • landscape/stack.js
  • lines 89 to 97
  • CivilCraft 0.8.1
export function levelReadout(l, surface) {
  if (!surface) return null;
  const z1 = surface.elevationAt(l.x, l.y);
  if (l.kind !== 'grade') return fin(z1) ? { z: z1 } : null;
  const z2 = surface.elevationAt(l.x2, l.y2);
  if (!fin(z1) || !fin(z2)) return null;
  const run = Math.hypot(l.x2 - l.x, l.y2 - l.y), dz = z2 - z1, g = Math.abs(dz) / run;
  return { z1, z2, dz, run, pct: g * 100, ratio: g > 1e-9 ? 1 / g : Infinity, down: dz <= 0 ? 'forward' : 'back' };
}
landscape/problems-landscape.js CivilCraft 0.8.1 64-bit 5 calculations Reviewed

Tests bin/test-landscape.mjs

Typical thresholds in project units

64-bit

The level difference that counts as meeting and the drop that counts as a trip edge.

meet = 0.01 m × k; drop = 0.15 m × k; k = 1 ÷ 0.3048 for feet

The thresholds are converted with the international foot, 0.3048, while the landscape engine's project foot is 1200 ÷ 3937 m. On a 10 mm threshold the difference is far below anything the check can show.

  • landscape/problems-landscape.js
  • line 40
  • CivilCraft 0.8.1
const out = [], k = units === 'm' ? 1 : 1 / 0.3048, meet = TYPICAL.meet * k, drop = TYPICAL.drop * k;

Paving level at a point

64-bit

The finished level of a paved area at any point: flat, on its cross-fall plane, or on its cone to a drain.

cross: z = z₀ − (pct ÷ 100)((x − cx)·cos a + (y − cy)·sin a); drain: z = z₀ + (pct ÷ 100) × distance to the drain

The plane is rebuilt here with the same formula as planeZ in grade.js, through the mean of the corners. One 64-bit expression per case.

  • landscape/problems-landscape.js
  • lines 26 to 35
  • CivilCraft 0.8.1
/** The paving's finished level at (x, y): its plane (cross fall), cone (to a drain) or flat level. */
export function pavingZ(p, surface, x, y) {
  const ps = pavingSurface(p, surface), f = p.falls;
  if (f.mode === 'cross') {   // the plane grade.js planeZ makes: through the outline's centroid at z0
    const cx = p.pts.reduce((v, q) => v + q.x, 0) / p.pts.length, cy = p.pts.reduce((v, q) => v + q.y, 0) / p.pts.length, a = (f.dir * Math.PI) / 180;
    return ps.z0 - (f.pct / 100) * ((x - cx) * Math.cos(a) + (y - cy) * Math.sin(a));
  }
  if (f.mode === 'drain' && f.drain) return ps.z0 + (f.pct / 100) * Math.hypot(x - f.drain.x, y - f.drain.y);
  return ps.z0;
}

Ground against a wall's top and footing

64-bit

Finds where the ground on the high side is above the wall's top or the ground on the low side is below the footing's top.

over = max(ground left, ground right) − top; footing shows = footing − min(ground left, ground right)

The ground is read at each wall station, thickness ÷ 2 plus 0.05 m (in project units) either side of the center line. A difference counts when it exceeds the 10 mm threshold. Only the largest difference along the wall is reported, and its text is rounded to 2 decimals.

  • landscape/problems-landscape.js
  • lines 52 to 58
  • CivilCraft 0.8.1
for (const st of lay.stations) {
  const gl = sf.elevationAt(st.x - st.uy * off, st.y + st.ux * off), gr = sf.elevationAt(st.x + st.uy * off, st.y - st.ux * off);
  if (gl == null || gr == null) continue;
  const hi = Math.max(gl, gr), lo = Math.min(gl, gr);
  if (hi - st.top > meet && (!over || hi - st.top > over.d)) over = { d: hi - st.top, st };
  if (st.footing - lo > meet && (!foot || st.footing - lo > foot.d)) foot = { d: st.footing - lo, st };
}

Steps against the paving they land on

64-bit

The level difference between a flight's bottom or top and the paving at that point.

d = z of the flight's end − paving level there

The paving is found by a point-in-polygon test 0.05 m (in project units) beyond the flight's end. A difference above the 10 mm threshold in either direction is reported.

  • landscape/problems-landscape.js
  • lines 77 to 80
  • CivilCraft 0.8.1
const pv = hard.find((h) => h.kind === 'paving' && pointIn(p.x + dir * ux * e, p.y + dir * uy * e, h.pts));   // just off the flight: where it lands
if (!pv) continue;
const d = z - pavingZ(pv, sf, p.x, p.y);
if (Math.abs(d) > meet) push('ls.j.step.' + end, 'warning', 'ls.j.step.' + end, { name: s.name, paving: pv.name, d: f2(Math.abs(d)), dir: t(d > 0 ? 'ls.j.above' : 'ls.j.below'), u: units }, [ref(s), ref(pv)], [p.x, p.y]);

Planted area level against a paving edge

64-bit

The largest level difference between a planted area's finished level and the paving along the edge they share.

d = area level − paving level at the midpoint of each cut edge

The shared edge comes from clipRegion; the paving level is taken at the midpoint of each cut piece only. The worst value by magnitude is compared with the 10 mm and 150 mm thresholds.

  • landscape/problems-landscape.js
  • lines 87 to 95
  • CivilCraft 0.8.1
const cut = clipRegion(a.pts, [pv.pts]).edges.filter((e) => e.from === 'cut');
if (!cut.length) continue;
let worst = null;
for (const e of cut) {
  const x = (e.a.x + e.b.x) / 2, y = (e.a.y + e.b.y) / 2, d = a.level - pavingZ(pv, sf, x, y);
  if (!worst || Math.abs(d) > Math.abs(worst.d)) worst = { d, x, y };
}
if (worst.d > meet) push('ls.j.bed.high', 'warning', 'ls.j.bed.high', { name: a.name, paving: pv.name, d: f2(worst.d), u: units }, [{ kind: 'ls-pa', id: a.id, label: a.name }, ref(pv)], [worst.x, worst.y]);
else if (-worst.d > drop) push('ls.j.bed.drop', 'note', 'ls.j.bed.drop', { name: a.name, paving: pv.name, d: f2(-worst.d), u: units }, [{ kind: 'ls-pa', id: a.id, label: a.name }, ref(pv)], [worst.x, worst.y]);

In detail

Method
Cut and fill: each grid cell goes to the last object in the stack whose outline holds its center; volume = (proposed − existing) × cell area. Overlap = area of the first outline minus what clipRegion leaves after the second is cut out. Gradient = level difference ÷ plan distance.
Number types
64-bit numbers.
Tolerances
Grid cell = √(box area ÷ 200000), doubled while the grid would pass 2000000 cells. An overlap is reported above 1e-6 of the first outline's area plus 1e-9. A gradient line needs two points more than 1e-9 apart. Junction thresholds: 0.01 m to meet and 0.15 m for a drop, converted to feet with 1 ÷ 0.3048.
Limits
Cut and fill are grid sums with no partial cells at the outlines. Wall checks are made at the wall's stations only. The thresholds are typical values, not a standard's, as the code comment says.

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