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