Resources Numerical accuracy Calculation policy

Pit and solar table meshes in the 3D view

The 3D view geometry for open pits (haul ramp ribbons, bench rims) and ground solar tables.

How the precision is kept

These functions only build triangles and line segments for the 3D view. Bench rims are returned as a Float32Array for drawing. The pit and ground figures a person reads are computed elsewhere in 64-bit numbers.

Calculations under this policy

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

site3d-pit.js CivilCraft 0.8.1 32-bit, display only 2 calculations Reviewed

Tests None found by the review

Bench rim line segments

32-bit, display only

Segment pairs for the rim of each bench.

Points are put through the view transform L and stored in a Float32Array that only feeds the GPU.

  • site3d-pit.js
  • lines 30 to 36
  • CivilCraft 0.8.1
export function pitLines(list, L) {
  const out = [];
  for (const pit of list || []) for (const b of pit.benches || []) for (const loop of b.loops) for (let i = 0; i < loop.length; i++) {
    const p = loop[i], q = loop[(i + 1) % loop.length]; out.push(...L(p[0], p[1], b.z), ...L(q[0], q[1], b.z));
  }
  return new Float32Array(out);
}

Haul ramp ribbon

32-bit, display only

A road-wide ribbon on the pit walls for the haul ramp.

offset = (−dy, dx) ÷ length × width ÷ 2; lift = 0.02 × width

A segment length of zero falls back to 1 so the division is guarded. The 0.02 lift is a drawing offset to avoid z-fighting.

  • site3d-pit.js
  • lines 12 to 28
  • CivilCraft 0.8.1
export function pitMesh(B, list, L) {
  let n = 0;
  for (const pit of list || []) {
    const r = pit.road; if (!r || r.pts.length < 2) continue;
    B.owner({ kind: 'pit', id: pit.id, name: pit.name });
    const h = (r.width || 0) / 2; if (!(h > 0)) continue;
    const side = r.pts.map((p, i) => { const a = r.pts[Math.max(0, i - 1)], b = r.pts[Math.min(r.pts.length - 1, i + 1)], dx = b[0] - a[0], dy = b[1] - a[1], l = Math.hypot(dx, dy) || 1; return [-dy / l * h, dx / l * h]; });
    for (let i = 0; i + 1 < r.pts.length; i++) {
      const a = r.pts[i], b = r.pts[i + 1], sa = side[i], sb = side[i + 1], lift = 0.02 * (r.width || 1);
      const q = [L(a[0] + sa[0], a[1] + sa[1], a[2] + lift), L(a[0] - sa[0], a[1] - sa[1], a[2] + lift), L(b[0] - sb[0], b[1] - sb[1], b[2] + lift), L(b[0] + sb[0], b[1] + sb[1], b[2] + lift)];
      const v = q.map((p) => B.vert(p, [0, 0, 1], RAMP));
      B.tri(v[0], v[1], v[2]); B.tri(v[0], v[2], v[3]); B.tri(v[0], v[2], v[1]); B.tri(v[0], v[3], v[2]);
    }
    n++;
  }
  return n;
}
site3d-solar.js CivilCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests None found by the review

Solar table triangle normal

32-bit, display only

A flat-shaded triangle with its normal.

n = (b − a) × (c − a) ÷ |…|

A 64-bit cross product; triangles whose normal length is 1e-12 or less are skipped.

  • site3d-solar.js
  • lines 12 to 19
  • CivilCraft 0.8.1
function tri(B, a, b, c, col, flip) {
  const ux = b[0] - a[0], uy = b[1] - a[1], uz = b[2] - a[2], vx = c[0] - a[0], vy = c[1] - a[1], vz = c[2] - a[2];
  let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx;
  const l = Math.hypot(nx, ny, nz); if (!(l > 1e-12)) return;
  if (flip) { nx = -nx; ny = -ny; nz = -nz; [b, c] = [c, b]; }
  const n = [nx / l, ny / l, nz / l];
  B.tri(B.vert(a, n, col), B.vert(b, n, col), B.vert(c, n, col));
}
pit-view.js CivilCraft 0.8.1 32-bit, display only 1 calculation Reviewed

Tests bin/test-civilcraft-pit.mjs

Pit scene in project units

32-bit, display only

Converts a pit's ramp and bench outlines from meters back to project units for the 3D view.

coordinate ÷ k, where k is meters per project unit

Divides 64-bit meter coordinates by the unit factor; the pit geometry itself comes from runPit and pitLoops, which this file only calls.

  • pit-view.js
  • lines 11 to 31
  • CivilCraft 0.8.1
export function pitScene(project) {
  const geo = project.state.geo, list = (geo?.pits || []).filter((p) => p.show3d !== 'no');
  if (!list.length) return { ground: null, pits: [], notes: [] };
  const units = project.state.units === 'm' ? 'm' : 'ft', k = toM(units), eg = project.eg().surface;
  const key = project.surfaceDeps() + '|' + units + '|' + JSON.stringify(list);
  return project.memo('pit3d', key, () => {
    let ground = eg, cutAny = false; const pits = [], notes = [];
    for (const p of list) {
      const r = runPit(p, units);
      pits.push({ id: p.id, name: p.name, road: r.road && r.road.pts.length ? { pts: r.road.pts.map((q) => [q[0] / k, q[1] / k, q[2] / k]), width: p.rampWidth } : null,
        benches: pitLoops(r).map((b) => ({ z: b.z / k, loops: b.loops.filter((l) => l.area > 0).map((l) => l.pts.map((q) => [q[0] / k, q[1] / k])) })) });
      for (const pr of r.problems) notes.push(pr);
      if (p.cut === 'yes' && ground) {
        const g = pitGround(ground, p, units, r);
        if (g) { ground = g.surface; cutAny = true; if (g.belowTop) notes.push({ code: 'pit.top.belowground', item: p.name || p.id }); }
        else notes.push({ code: 'pit.cut.failed', item: p.name || p.id });
      }
    }
    return { ground: cutAny ? ground : null, pits, notes };
  });
}

In detail

Method
Ramp ribbons offset by half the road width along the segment normal; solar tables are two triangles per table with normals from a cross product.
Number types
64-bit arrays are built and then handed to the mesh builder; pitLines ends in a Float32Array.
Tolerances
Triangles with normal length 1e-12 or less are skipped; piles are drawn only where the table is more than 1e-3 above the ground.

Approach: Display only

Computed to draw the screen, often in 32-bit on the graphics processor. No number a person reads, saves or exports comes from it.

Every policy that takes this approach