Resources Numerical accuracy Calculation policy

Section cut from the ground along any line

Cuts the ground surface along a polyline and gives the distance, offset and elevation at every triangle edge it crosses; also measures along, locates on and crosses polylines.

How the precision is kept

The cut has a point wherever the line crosses a triangle edge, so it follows the triangulated surface with no sampling. Coordinates are shifted by the surface's origin before the crossings are computed. Distances along the line are a running 64-bit sum of segment lengths plus a fraction of the current segment. Points closer than 1e-9 of the line's length are merged.

Open item. Scale the 1e-15 parallel test in segCross to the lengths of the two segments.

Calculations under this policy

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

terrain-section.js CivilCraft 0.8.1 64-bit 8 calculations Reviewed

Tests bin/test-civilcraft-river.mjs

Length of a polyline

64-bit

The plan length of a polyline.

L = Σ √(Δx² + Δy²)

A running 64-bit sum of Math.hypot segment lengths in order, with no compensation.

  • terrain-section.js
  • lines 29 to 33
  • CivilCraft 0.8.1
export function lineLength(line) {
  let L = 0;
  for (let i = 1; i < (line?.length || 0); i++) L += Math.hypot(line[i].x - line[i - 1].x, line[i].y - line[i - 1].y);
  return L;
}

Point at a distance along a line

64-bit

The point and direction at a distance along a polyline.

u = (d − length before the segment) ÷ segment length, clamped to [0, 1]; p = a + (b − a)·u

The distance before the segment is a running sum, then one division and one interpolation. Zero-length segments are skipped before the division. The direction is Math.atan2 of the segment.

  • terrain-section.js
  • lines 35 to 48
  • CivilCraft 0.8.1
export function pointAt(line, d) {
  let acc = 0;
  for (let i = 1; i < line.length; i++) {
    const p = line[i - 1], q = line[i], l = Math.hypot(q.x - p.x, q.y - p.y);
    if (l <= 0) continue;
    if (d <= acc + l || i === line.length - 1) {
      const u = Math.max(0, Math.min(1, (d - acc) / l));
      return { x: p.x + (q.x - p.x) * u, y: p.y + (q.y - p.y) * u, dir: Math.atan2(q.y - p.y, q.x - p.x) };
    }
    acc += l;
  }
  const p = line[0];
  return { x: p.x, y: p.y, dir: 0 };
}

Nearest point on a line

64-bit

The distance along a polyline and the offset to the side of the point nearest to a given point.

u = ((p − a)·(b − a)) ÷ |b − a|², clamped; d = length before + u·|b − a|; offset = ± distance, sign from the cross product

Dot and cross products of coordinate differences in 64-bit; the differences are taken from the segment's start, so they stay small. A later segment replaces the best only when it is closer by more than 1e-12.

  • terrain-section.js
  • lines 50 to 66
  • CivilCraft 0.8.1
export function locateOn(line, p) {
  let best = null, 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);
    if (l2 <= 0) continue;
    const u = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / l2));
    const x = a.x + dx * u, y = a.y + dy * u, dist = Math.hypot(p.x - x, p.y - y);
    if (!best || dist < best.dist - 1e-12) {
      const cross = dx * (p.y - a.y) - dy * (p.x - a.x);   // > 0: p is to the left
      best = { d: acc + u * l, off: cross > 0 ? -dist : dist, x, y, dir: Math.atan2(dy, dx), dist };
    }
    acc += l;
  }
  if (!best) return null;
  delete best.dist;
  return best;
}

Crossing of two segments

64-bit

Where two segments cross, as fractions along each.

den = r × s; t = ((q − p) × s) ÷ den; u = ((q − p) × r) ÷ den

Plain 64-bit cross products. Segments count as parallel when |den| < 1e-15, an absolute value not scaled to their lengths. Fractions within 1e-12 outside [0, 1] are accepted and clamped.

  • terrain-section.js
  • lines 68 to 73
  • CivilCraft 0.8.1
function segCross(px, py, rx, ry, qx, qy, sx, sy) {   // p + t·r vs q + u·s → [t, u] in [0,1]² or null
  const den = rx * sy - ry * sx;
  if (Math.abs(den) < 1e-15) return null;
  const t = ((qx - px) * sy - (qy - py) * sx) / den, u = ((qx - px) * ry - (qy - py) * rx) / den;
  return t >= -EPS && t <= 1 + EPS && u >= -EPS && u <= 1 + EPS ? [Math.max(0, Math.min(1, t)), Math.max(0, Math.min(1, u))] : null;
}

First crossing of two polylines

64-bit

The first place one polyline crosses another, with the distance along each.

d_a = length before + t·|segment a|; d_b = length before + u·|segment b|

Distances are running sums of segment lengths plus the crossing fraction times the segment length. The crossing point is interpolated on the first polyline.

  • terrain-section.js
  • lines 75 to 90
  • CivilCraft 0.8.1
export function crossing(a, b) {
  let accA = 0;
  for (let i = 1; i < a.length; i++) {
    const p = a[i - 1], q = a[i], la = Math.hypot(q.x - p.x, q.y - p.y);
    let accB = 0, hit = null;
    for (let j = 1; j < b.length; j++) {
      const r = b[j - 1], s = b[j], lb = Math.hypot(s.x - r.x, s.y - r.y);
      const c = segCross(p.x, p.y, q.x - p.x, q.y - p.y, r.x, r.y, s.x - r.x, s.y - r.y);
      if (c && (!hit || c[0] < hit.t)) hit = { t: c[0], da: accA + c[0] * la, db: accB + c[1] * lb, x: p.x + (q.x - p.x) * c[0], y: p.y + (q.y - p.y) * c[0] };
      accB += lb;
    }
    if (hit) { delete hit.t; return hit; }
    accA += la;
  }
  return null;
}

Line square to a polyline

64-bit

A straight line square to a polyline at a distance along it, a set length to each side.

right = (sin dir, −cos dir); p = c − right × left; q = c + right × right length

64-bit sine and cosine of the segment direction, one multiply and add per coordinate.

  • terrain-section.js
  • lines 92 to 96
  • CivilCraft 0.8.1
export function squareAcross(line, d, left, right) {
  const p = pointAt(line, d);
  const rx = Math.sin(p.dir), ry = -Math.cos(p.dir);   // right of the direction (y up)
  return [{ x: p.x - rx * left, y: p.y - ry * left }, { x: p.x + rx * right, y: p.y + ry * right }];
}

Section along a line

64-bit

The points of the ground along a line: distance, offset, coordinates and elevation at every triangle edge crossing and line vertex.

d = length before + t·|segment|; o = d − origin; z from the surface at (x, y)

Crossings are found in coordinates local to the surface's origin, while the point itself is interpolated in project coordinates. Points closer than max(1e-9 × line length, 1e-12) are merged, a tolerance that scales with the line. Elevations come from the surface's lookup with tolerance 1e-7.

  • terrain-section.js
  • lines 137 to 174
  • CivilCraft 0.8.1
export function cutSection(surface, line, { origin = 0 } = {}) {
  const length = lineLength(line);
  const res = { length, origin, runs: [], breaks: 0 };
  if (!surface || !Array.isArray(line) || line.length < 2 || !(length > 0)) return res;
  const { ox, oy } = surface;
  const cand = [];   // { d, x, y, k }
  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);
    if (!(l > 0)) continue;
    cand.push({ d: acc, x: a.x, y: a.y, k: i === 1 ? 'end' : 'vertex' });
    for (const t of edgeParams(surface, a.x - ox, a.y - oy, b.x - ox, b.y - oy)) {
      if (t <= EPS || t >= 1 - EPS) continue;
      cand.push({ d: acc + t * l, x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, k: 'edge' });
    }
    acc += l;
  }
  const last = line[line.length - 1];
  cand.push({ d: length, x: last.x, y: last.y, k: 'end' });
  cand.sort((u, v) => u.d - v.d);
  const tol = Math.max(1e-9 * length, 1e-12);
  let run = null, prev = -Infinity;
  for (const c of cand) {
    if (c.d - prev < tol) {   // merge: keep the stronger kind (end > vertex > edge)
      const r = run?.[run.length - 1];
      if (r && c.k !== 'edge') r.k = r.k === 'end' || c.k === 'end' ? 'end' : 'vertex';
      continue;
    }
    prev = c.d;
    const z = surface.elevationAt(c.x, c.y, 1e-7);
    if (z === null) { run = null; continue; }
    if (!run) res.runs.push((run = []));
    run.push({ d: c.d, o: c.d - origin, x: c.x, y: c.y, z, k: c.k });
    if (c.k === 'edge') res.breaks++;
  }
  res.runs = res.runs.filter((r) => r.length >= 2);
  return res;
}

Elevation of a section at an offset

64-bit

The elevation of a cut section at any offset, between its points.

z = z₀ + (z₁ − z₀)·(o − o₀) ÷ (o₁ − o₀)

Linear interpolation in 64-bit; a zero-width interval returns the second point's elevation instead of dividing. Offsets are matched with an absolute 1e-9.

  • terrain-section.js
  • lines 176 to 184
  • CivilCraft 0.8.1
export function zAt(runs, o) {
  for (const r of runs) {
    if (o < r[0].o - 1e-9 || o > r[r.length - 1].o + 1e-9) continue;
    for (let i = 1; i < r.length; i++) {
      if (o <= r[i].o + 1e-9) { const p = r[i - 1], q = r[i], d = q.o - p.o; return d > 0 ? p.z + ((q.z - p.z) * (o - p.o)) / d : q.z; }
    }
  }
  return null;
}

In detail

Method
Segment against triangle edge crossings by parameter (cells of the surface's grid along the segment, Liang-Barsky box test), elevation from the surface's own lookup.
Number types
64-bit numbers.
Tolerances
Parallel test |denominator| < 1e-15 (absolute); crossing parameters accepted within 1e-12 of [0, 1]; merge distance max(1e-9 × length, 1e-12); elevation lookup tolerance 1e-7; offsets matched within 1e-9; nearest-point ties broken at 1e-12.
Limits
The crossing test is plain 64-bit arithmetic, not an exact predicate, and its parallel test is not scaled to the segment lengths.

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.

Every policy that takes this approach