Resources Numerical accuracy Calculation policy

Landscape sections and detail drawings

Cuts a section line through the ground, walls, paving, planted areas, steps and plants and lays the result out to scale on a sheet, and draws wall, paving and step details from their numbers.

How the precision is kept

Arithmetic is 64-bit. Where the section line meets outlines is found by segment intersection, with distances along the line rounded to 9 decimals. What is drawn at those places is partly sampled: a wall takes the values of its nearest station, a flight of steps is sampled along the cut, and a build-up follows up to 200 points of the proposed ground. Sheets use standard scales with 304.8 mm per project foot.

Calculations under this policy

11 calculations in 3 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/section.js CivilCraft 0.8.1 64-bit 5 calculations Reviewed

Tests bin/test-landscape.mjs

Where the section line is inside an outline

64-bit

The stretches of the section line, as distances along it, that lie inside a closed outline.

d = distance to the segment start + t × segment length, for every crossing t

Crossing distances are rounded with Math.round(d × 1e9) ÷ 1e9 before they are sorted and paired, and that rounded value is what the bands are drawn from. A stretch is kept when a point-in-polygon test at its midpoint passes.

  • landscape/section.js
  • lines 20 to 40
  • CivilCraft 0.8.1
export function crossIntervals(line, poly) {
  if (!poly || poly.length < 3 || line.length < 2) return [];
  const ds = [];
  let acc = 0;
  for (let i = 1; i < line.length; i++) {
    const a = line[i - 1], b = line[i], l = Math.hypot(b.x - a.x, b.y - a.y);
    ds.push(acc);
    for (let j = 0; j < poly.length; j++) { const t = segCross(a, b, poly[j], poly[(j + 1) % poly.length]); if (t !== null) ds.push(acc + t * l); }
    acc += l;
  }
  ds.push(acc);
  const D = [...new Set(ds.map((v) => Math.round(v * 1e9) / 1e9))].sort((p, q) => p - q), out = [];
  const at = (d) => { let s = 0; for (let i = 1; i < line.length; i++) { const a = line[i - 1], b = line[i], l = Math.hypot(b.x - a.x, b.y - a.y); if (d <= s + l || i === line.length - 1) { const f = l ? (d - s) / l : 0; return { x: a.x + f * (b.x - a.x), y: a.y + f * (b.y - a.y) }; } s += l; } return line[0]; };
  for (let k = 0; k + 1 < D.length; k++) {
    if (D[k + 1] - D[k] < 1e-9) continue;
    const m = at((D[k] + D[k + 1]) / 2);
    if (!pointIn(m.x, m.y, poly)) continue;
    if (out.length && Math.abs(out[out.length - 1][1] - D[k]) < 1e-9) out[out.length - 1][1] = D[k + 1]; else out.push([D[k], D[k + 1]]);
  }
  return out;
}

Where the section line crosses an open line

64-bit

The distances along the section line where it crosses a wall or other open line.

d = distance to the segment start + t × segment length

Uses segCross from areas.js for each pair of segments. Distances are rounded to 9 decimals and duplicates removed.

  • landscape/section.js
  • lines 42 to 51
  • CivilCraft 0.8.1
export function crossAt(line, pl) {
  const out = [];
  let acc = 0;
  for (let i = 1; i < line.length; i++) {
    const a = line[i - 1], b = line[i], l = Math.hypot(b.x - a.x, b.y - a.y);
    for (let j = 1; j < pl.length; j++) { const t = segCross(a, b, pl[j - 1], pl[j]); if (t !== null) out.push(acc + t * l); }
    acc += l;
  }
  return [...new Set(out.map((v) => Math.round(v * 1e9) / 1e9))].sort((p, q) => p - q);
}

Distance along and across the section line

64-bit

How far along the section line a point is and how far to its side, used to pick the plants drawn in the section.

t = clamp(((p − a)·(b − a)) ÷ |b − a|², 0, 1); offset = ±|p − (a + t(b − a))|

One clamped projection per segment; the side comes from the sign of a cross product. The nearest segment wins.

  • landscape/section.js
  • lines 53 to 63
  • CivilCraft 0.8.1
export function offsetFrom(line, p) {
  let best = { d: 0, off: Infinity }, acc = 0;
  for (let i = 1; i < line.length; i++) {
    const a = line[i - 1], b = line[i], dx = b.x - a.x, dy = b.y - a.y, L2 = dx * dx + dy * dy, l = Math.sqrt(L2);
    const t = L2 ? Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / L2)) : 0;
    const off = Math.hypot(p.x - a.x - t * dx, p.y - a.y - t * dy);
    if (off < Math.abs(best.off)) best = { d: acc + t * l, off: ((dx * (p.y - a.y) - dy * (p.x - a.x)) >= 0 ? 1 : -1) * off };
    acc += l;
  }
  return best;
}

Proposed ground level at a distance

64-bit

The level of the proposed ground at a distance along the section, between the cut's points.

z = z₀ + (d − d₀) ÷ (d₁ − d₀) × (z₁ − z₀)

Linear interpolation between neighboring cut points, accepted up to 1e-9 beyond either end. A zero-length step returns the first point's level.

  • landscape/section.js
  • lines 65 to 69
  • CivilCraft 0.8.1
/** The proposed ground's z at distance d (linear between the cut's points), or null off the surface. */
function zOn(runs, d) {
  for (const r of runs || []) for (let i = 1; i < r.length; i++) if (d >= r[i - 1].d - 1e-9 && d <= r[i].d + 1e-9) { const f = r[i].d - r[i - 1].d ? (d - r[i - 1].d) / (r[i].d - r[i - 1].d) : 0; return r[i - 1].z + f * (r[i].z - r[i - 1].z); }
  return null;
}

Build-up layers under the ground line

64-bit

Each layer of a paving or planted-area build-up drawn as a band hanging below the proposed ground.

top of layer = ground − Σ thickness of the layers above; y = z × vertical exaggeration

The ground is sampled at between 2 and 200 evenly spaced points across the band, so a break in the ground between samples is cut straight. Layer depths are added in order.

  • landscape/section.js
  • lines 76 to 86
  • CivilCraft 0.8.1
// build-ups: each layer a band hanging below the proposed ground, sampled along the interval
for (const b of input.bands || []) {
  const n = Math.max(2, Math.min(200, Math.ceil((b.d1 - b.d0) / Math.max(1e-6, (input.length || 1) / 200)) + 1)), ds = Array.from({ length: n }, (_, i) => b.d0 + ((b.d1 - b.d0) * i) / (n - 1));
  const top = ds.map((d) => zOn(input.proposed, d));
  if (top.some((z) => z === null)) continue;
  let depth = 0;
  for (const L of b.layers) {
    const a = ds.map((d, i) => ({ x: d, y: Y(top[i] - depth) })), c = ds.map((d, i) => ({ x: d, y: Y(top[i] - depth - L.thickness) })).reverse();
    P([...a, ...c], 'L-SECT-BLDP', { closed: true });
    depth += L.thickness;
  }
landscape/section-ls.js CivilCraft 0.8.1 64-bit 4 calculations Reviewed

Tests bin/test-landscape.mjs

Wall at the section line

64-bit

The top and footing levels of a wall where the section line crosses it.

values of the wall station nearest the crossing point

The crossing point is placed by its distance ÷ the line length along the section's two end points. The wall's top and footing are copied from the nearest station of the wall layout, not interpolated between stations. The footing depth drawn is capped at 0.3 m or 1 ft.

  • landscape/section-ls.js
  • lines 55 to 58
  • CivilCraft 0.8.1
for (const d of crossAt(line, w.pts)) {
  const f = d / L, p = { x: line[0].x + f * (line[1].x - line[0].x), y: line[0].y + f * (line[1].y - line[0].y) };
  const st = lay.stations.reduce((b, s) => (!b || Math.hypot(s.x - p.x, s.y - p.y) < Math.hypot(b.x - p.x, b.y - p.y) ? s : b), null);
  if (st) input.walls.push({ d, top: st.top, footing: st.footing, thickness: w.thickness, footingWidth: w.footing.width, footingDepth: Math.min(w.footing.depth, units === 'm' ? 0.3 : 1), name: w.name });

Steps in profile

64-bit

The stepped outline of a flight where the section line passes through it.

risers climbed = ⌊distance along the flight ÷ going⌋ + 1; z = bottom + risers × riser

The profile is sampled at steps of max(stretch ÷ 400, going ÷ 8), built by repeated addition, so each riser is drawn within one such step of its true position. Math.floor decides the riser count with no tolerance.

  • landscape/section-ls.js
  • lines 73 to 78
  • CivilCraft 0.8.1
const prof = [], step = Math.max((d1 - d0) / 400, lay.going / 8);
for (let d = d0; d <= d1 + 1e-9; d += step) {
  const f = Math.min(d, d1) / L, p = { x: line[0].x + f * (line[1].x - line[0].x), y: line[0].y + f * (line[1].y - line[0].y) };
  const sAlong = (p.x - s.pts[0].x) * lay.ux + (p.y - s.pts[0].y) * lay.uy;
  const k = sAlong >= lay.run ? lay.n : Math.max(0, Math.min(lay.n, Math.floor(sAlong / lay.going) + 1));
  prof.push({ d: Math.min(d, d1), z: lay.zB + k * lay.riser });

Plants near the section line

64-bit

The plants drawn in a section, with their height and spread in project units.

h = height (m) ÷ k; k = 0.3048 for feet

Plant sizes in meters are divided by 0.3048 in a feet project, while planting.js converts with 1200 ÷ 3937. A plant is drawn when its offset from the line is within the section's band and it lies strictly between the line's ends.

  • landscape/section-ls.js
  • lines 89 to 95
  • CivilCraft 0.8.1
const show = ls.show || 'mature', k = units === 'm' ? 1 : 0.3048;
for (const p of placed(ls, index, units, sf ? (x, y) => sf.elevationAt(x, y) : null)) {
  const sp = index.get(p.key);
  if (!sp) continue;
  const o = offsetFrom(line, p), sz = sizeOf(sp, show);
  if (Math.abs(o.off) > sec.band || o.d <= 0 || o.d >= L || !fin(p.z)) continue;
  input.plants.push({ d: o.d, z: p.z, h: sz.h / k, s: sz.s / k, tree: sp.type === 'tree', name: sp.botanical });

Scale of a section on its sheet

64-bit

Picks the standard scale a section is drawn at and the paper millimeters per project unit.

k = real ÷ n; real = 1000 mm per m or 304.8 mm per ft; n = first of 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000 with real ÷ n ≤ fit

The scale is one of a fixed list, or 1:5000 when none fits. A project foot is drawn as 304.8 mm, the international foot. The drawing's width and height are floored at 1e-6 before the fit is divided out.

  • landscape/section-ls.js
  • lines 123 to 127
  • CivilCraft 0.8.1
const w = Math.max(1e-6, b.x1 - b.x0), h = Math.max(1e-6, b.y1 - b.y0);
const fit = Math.min((cellW - 16) / w, (cellH - 24) / h), real = o.units === 'm' ? 1000 : 304.8;
const std = [10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000].find((n) => real / n <= fit) || 5000, k = real / std;
sh.chromeTexts.push({ color: '#000000', x: cx0 + 4, y: cy1 - 10, h: 1.8, rot: 0, text: t('ls.sec.scale', { scale: '1:' + std, vex: sec.vex }), align: 'left' });
const ox = cx0 + 8 - b.x0 * k, oy = cy1 - cellH + 10 - b.y0 * k;
landscape/sheets.js CivilCraft 0.8.1 64-bit 2 calculations Reviewed

Tests bin/test-landscape.mjs

Scale of a detail

64-bit

Picks the standard scale of a wall, paving or steps detail and the paper millimeters per project unit.

k = per ÷ n; per = 1000 or 304.8; n = first of 5, 10, 20, 25, 50, 100 with 1 ÷ n ≤ fit ÷ per

The scale comes from a fixed list and falls back to 1:100. Every dimension of the detail is then one multiplication by k.

  • landscape/sheets.js
  • line 125
  • CivilCraft 0.8.1
const drawIn = (w, h) => { const sc = Math.min((cellW - 20) / w, (cellH - 28) / h); const sStd = [5, 10, 20, 25, 50, 100].find((n) => 1 / n <= sc / per) || 100; return { k: per / sStd, label: '1:' + sStd }; };   // mm on paper per project unit

Wall detail: batter and footing as drawn

64-bit

The lean of the wall face and the footing shown in a wall's detail drawing.

batter offset = tan(batter°) × (top − footing) × k; footing drawn = min(0.6 × depth, 0.3 m)

The batter angle in degrees is multiplied by Math.PI ÷ 180. The footing thickness drawn is schematic: the smaller of 0.6 of the footing depth and 0.3 m, with the feet value written as 0.3 × 3.28.

  • landscape/sheets.js
  • lines 129 to 133
  • CivilCraft 0.8.1
const w = d.wall, H = st.top - st.footing, W = w.footing.width * 2.2;
const { k, label } = drawIn(W, H + 0.3 * H);
const bx = cx0 + cellW / 2, by = cy1 - cellH + 18, z = (v) => by + (v - st.footing) * k;
const half = (w.thickness / 2) * k, fw = (w.footing.width / 2) * k, ft = Math.min(w.footing.depth * 0.6, 0.3 * (o.units === 'm' ? 1 : 3.28)) * k;
const bat = Math.tan((w.batter * Math.PI) / 180) * (st.top - st.footing) * k;

In detail

Method
Intervals inside an outline come from all crossings of the line with the outline's edges, kept where the midpoint is inside. The proposed ground between cut points is linear. Sheet scale is the first standard scale that fits the cell.
Number types
64-bit numbers.
Tolerances
Distances along the section line rounded to 1e-9; intervals shorter than 1e-9 dropped; ground lookup allows 1e-9 beyond a run's ends. A section line shorter than 1e-6 is refused.
Limits
Wall top and footing are those of the nearest wall station, not interpolated. Plant sizes in a feet project are divided by 0.3048, and sheet scales use 304.8 mm per foot, while the landscape engine's project foot is 1200 ÷ 3937 m. The details sheet draws some dimensions schematically.

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