Reads a slope as typed, and works out the length, face area and heights of the retaining walls around a pad and the warnings for tall walls and steep slopes.
How the precision is kept
64-bit closed forms. A typed slope is converted to horizontal run per unit rise without rounding. Wall length and face area are sums over the wall's points along the pad edge. Heights and limits are rounded only in the label and warning text.
Calculations under this policy
4 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.
Turns a typed slope (ratio, percent, degrees or vertical) into horizontal run per unit rise.
percent: H = 100 ÷ p; degrees: H = 1 ÷ tan(a·π ÷ 180); ratio: H = h ÷ v
The text is read with Number and converted in one 64-bit expression; nothing is rounded. Zero and negative entries return no value instead of dividing.
sides.js
lines 69 to 82
CivilCraft 0.8.1
/** A slope as typed: H:V ratio ("3", "3:1"), percent ("33%"), degrees ("18.4°", "18.4 deg") or "vertical" (0). → H per 1 V. */
export function parseSlope(text) {
const s = String(text ?? '').trim().toLowerCase().replace(',', '.');
if (!s) return null;
if (/^(vertical|wall|vert)$/.test(s)) return 0;
let m = /^([\d.]+)\s*%$/.exec(s);
if (m) { const p = Number(m[1]); return p > 0 ? 100 / p : null; }
m = /^([\d.]+)\s*(°|deg|degrees?)$/.exec(s);
if (m) { const a = Number(m[1]); return a > 0 && a < 90 ? 1 / Math.tan((a * Math.PI) / 180) : a >= 90 ? 0 : null; }
m = /^([\d.]+)\s*:\s*([\d.]+)$/.exec(s);
if (m) { const h = Number(m[1]), v = Number(m[2]); return h > 0 && v > 0 ? h / v : null; }
const h = Number(s);
return Number.isFinite(h) && h > 0 ? h : null;
}
Wall height at each point
64-bit
The top, bottom and height of a retaining wall at each point along the pad edge.
h = |z_pad − z_ground|; top = max; bottom = min
One 64-bit subtraction per point from the pad and ground elevations the grading supplies.
sides.js
lines 90 to 107
CivilCraft 0.8.1
/** Consecutive wall rays (in order round the pad) as runs; a run that goes round the first corner is joined up. */
export function wallRuns(rays) {
const runs = [];
let cur = null;
for (const r of rays) {
if (r.kind !== 'wall') { cur = null; continue; }
if (!cur) runs.push((cur = { k: [], pts: [] }));
if (!cur.k.includes(r.k)) cur.k.push(r.k);
const zPad = r.z0, zGround = r.z;
cur.pts.push({ x: r.x0, y: r.y0, ox: r.x, oy: r.y, zPad, zGround, top: Math.max(zPad, zGround), bottom: Math.min(zPad, zGround), h: Math.abs(zPad - zGround) });
}
if (runs.length > 1 && rays.length && rays[0].kind === 'wall' && rays[rays.length - 1].kind === 'wall') {
const first = runs.shift(), last = runs[runs.length - 1];
last.pts.push(...first.pts);
for (const k of first.k) if (!last.k.includes(k)) last.k.push(k);
} else if (runs.length === 1 && rays.every((r) => r.kind === 'wall')) runs[0].closed = true; // walls all round
return runs;
}
Wall length and face area
64-bit
The total length, face area, greatest height and average height of the walls.
length = Σ l; face = Σ l·(h₁ + h₂) ÷ 2; average height = face ÷ length
Running 64-bit sums over the wall segments in order, with Math.hypot for each segment length along the pad edge. The average height is guarded against zero length.
sides.js
lines 109 to 117
CivilCraft 0.8.1
export function wallQuantities(walls) {
let length = 0, faceArea = 0, maxH = 0;
for (const w of walls || []) {
const P = w.pts, n = P.length, m = w.closed ? n : n - 1;
for (let i = 0; i < m; i++) { const a = P[i], b = P[(i + 1) % n], l = Math.hypot(b.x - a.x, b.y - a.y); length += l; faceArea += (l * (a.h + b.h)) / 2; }
for (const p of P) maxH = Math.max(maxH, p.h);
}
return { n: (walls || []).length, length, faceArea, maxH, avgH: length > 0 ? faceArea / length : 0 };
}
Tall wall and steep slope checks
64-bit
Flags a wall higher than the limit and a cut or fill slope steeper than the limit.
wall: h > limit + 1e-9; slope: H < max slope − 1e-9
Comparisons in 64-bit with an absolute tolerance of 1e-9 in project units. The height and limits in the warning text are rounded to 2 decimals; the unrounded numbers are returned alongside.
sides.js
lines 119 to 137
CivilCraft 0.8.1
export function sideWarnings(dl, pad) {
const S = pad.sides, out = [], u = S?.u || 'ft', name = pad.name || t('scene.pad.default'), ref = pad.id ?? null;
if (!S) return out;
for (const w of dl.walls || []) {
const h = Math.max(0, ...w.pts.map((p) => p.h));
const vars = { name, h: h.toFixed(2), limit: S.wallLimit.toFixed(2), u };
if (h > S.wallLimit + 1e-9) out.push({ code: 'grade.wall.tall', text: t('sides.w.tall', vars), hard: false, severity: 'warning', msg: { key: 'sides.w.tall', vars }, ref, about: pad.about || null, k: w.k, h, limit: S.wallLimit });
}
if (S.maxSlope > 0) {
const steep = new Map(); // side k → { kind, H }
for (const r of dl.rays) {
if ((r.kind !== 'cut' && r.kind !== 'fill') || !(r.k >= 0) || !(r.d > 1e-6)) continue;
const e = S.edges?.[r.k], H = r.kind === 'cut' ? (e?.cut || Number(pad.cut)) : (e?.fill || Number(pad.fill));
if (H < S.maxSlope - 1e-9 && !steep.has(r.k + r.kind)) steep.set(r.k + r.kind, { k: r.k, kind: r.kind, H });
}
for (const s of steep.values()) { const vars = { name, side: s.k + 1, h: s.H.toFixed(2), max: S.maxSlope.toFixed(2) }; out.push({ code: 'grade.slope.steep', text: t('sides.w.steep.' + s.kind, vars), hard: false, severity: 'warning', msg: { key: 'sides.w.steep.' + s.kind, vars }, ref, about: pad.about || null, k: [s.k], kind: s.kind, H: s.H, limit: S.maxSlope }); }
}
return out;
}
In detail
Method
Slope from ratio, percent (100 ÷ p) or degrees (1 ÷ tan); wall face area by the trapezoid rule on the heights at neighboring points.
Number types
64-bit numbers.
Tolerances
A wall is tall when its height exceeds the limit by more than 1e-9; a slope is steep when it is below the limit by more than 1e-9; daylight rays with a length value of 1e-6 or less are ignored. All three are absolute.
Limits
Wall heights are known only at the daylight rays, so the face area is a trapezoid sum between them.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.