Resources Numerical accuracy Calculation policy

Water mains as objects: depths, fittings and take-off

Places each water main at the ground less the cover, derives the bends, tees, reducers and caps the layout implies, and lists pipe lengths and fitting counts.

How the precision is kept

Pipe ends are the node's ground elevation minus the cover in 64-bit, and a pipe's length is its stated length or the plan distance between its nodes. Three values are rounded before they are kept: a new node's ground elevation is rounded to 0.01 when it is placed, a bend's angle is rounded to 0.1 degree, and pipe lengths in the take-off CSV are rounded to 0.01.

Open item. Keep the unrounded ground elevation on a new node and round it only where it is shown.

Calculations under this policy

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

water-objects.js CivilCraft 0.8.1 64-bit 4 calculations Reviewed

Tests bin/test-civilcraft-waterobjects.mjs

Turn angle at a junction

64-bit

The angle a main turns through at a junction of two pipes.

turn = |atan2 of the leg after − atan2 of the leg before|, folded to 0 to 180 degrees

Two Math.atan2 calls and a subtraction in 64-bit; no acos, so nothing to clamp.

  • water-objects.js
  • lines 44 to 48
  • CivilCraft 0.8.1
const turnDeg = (p, q, r) => {   // the angle a line turns at q going p → q → r
  const a = Math.atan2(q.y - p.y, q.x - p.x), b = Math.atan2(r.y - q.y, r.x - q.x);
  let d = Math.abs(b - a); if (d > Math.PI) d = 2 * Math.PI - d;
  return (d * 180) / Math.PI;
};

Pipe depths and derived fittings

64-bit

The elevation of each pipe end, the position of placed objects along pipes, and the fittings the layout implies.

z = ground − cover; on a pipe: p = a + (b − a)·t; length = stated, else √(Δx² + Δy²); bend angle rounded to 0.1°

Elevations and positions are single 64-bit subtractions and interpolations. A bend's angle is rounded to one decimal of a degree before it is stored on the fitting. A reducer is found by comparing the two pipes' sizes for exact equality.

  • water-objects.js
  • lines 50 to 82
  • CivilCraft 0.8.1
export function layout(water, units, { cover = null, zAt = null } = {}) {
  const U = units === 'm' ? 'm' : 'ft', C = cover ?? COVER[U];
  const nodes = water?.nodes || [], by = new Map(nodes.map((n) => [n.id, n]));
  const ground = (n) => (n.z != null ? n.z : zAt ? num(zAt(n.x, n.y), null) : null);
  const at = (n) => { const g = ground(n); return { x: n.x, y: n.y, z: g == null ? null : n.kind === 'reservoir' || n.kind === 'tank' ? g : g - C }; };
  const pipes = [], deg = new Map(nodes.map((n) => [n.id, []]));
  for (const p of water?.pipes || []) {
    const a = by.get(p.a), b = by.get(p.b);
    if (!a || !b) continue;
    pipes.push({ id: p.id, name: p.name || p.id, a: at(a), b: at(b), d: p.d ?? null, len: p.length ?? Math.hypot(a.x - b.x, a.y - b.y), kind: p.kind, status: p.status });
    deg.get(a.id).push({ pipe: p, other: b }); deg.get(b.id).push({ pipe: p, other: a });
  }
  const fixtures = [];
  // placed objects
  for (const f of water?.fittings || []) {
    if (f.host.node) { const n = by.get(f.host.node), pt = n && at(n); if (pt) fixtures.push({ id: f.id, kind: f.kind, source: 'placed', ...pt, dia: f.dia ?? null, name: f.name || f.id, host: f.host, label: f.name || '' }); }
    else { const p = pipes.find((x) => x.id === f.host.pipe); if (p) { const t = f.host.t; fixtures.push({ id: f.id, kind: f.kind, source: 'placed', x: p.a.x + (p.b.x - p.a.x) * t, y: p.a.y + (p.b.y - p.a.y) * t, z: p.a.z == null || p.b.z == null ? null : p.a.z + (p.b.z - p.a.z) * t, dia: f.dia ?? p.d, name: f.name || f.id, host: f.host, label: f.name || '' }); } }
  }
  // derived from the layout (junctions only: a reservoir or tank has its own outlet)
  for (const n of nodes) {
    if (n.kind !== 'junction') continue;
    const links = deg.get(n.id).filter((l) => l.pipe.kind === 'pipe'), k = links.length, pt = at(n);
    const base = { source: 'derived', ...pt, host: { node: n.id }, name: n.name || n.id, label: '' };
    if (k >= 4) fixtures.push({ id: 'cross:' + n.id, kind: 'cross', dia: Math.max(...links.map((l) => l.pipe.d || 0)) || null, ...base });
    else if (k === 3) fixtures.push({ id: 'tee:' + n.id, kind: 'tee', dia: Math.max(...links.map((l) => l.pipe.d || 0)) || null, ...base });
    else if (k === 2) {
      const [l1, l2] = links, ang = turnDeg(l1.other, n, l2.other);
      if ((l1.pipe.d || 0) !== (l2.pipe.d || 0) && l1.pipe.d && l2.pipe.d) fixtures.push({ id: 'reducer:' + n.id, kind: 'reducer', dia: Math.max(l1.pipe.d, l2.pipe.d), dia2: Math.min(l1.pipe.d, l2.pipe.d), ...base });
      if (ang >= 5) fixtures.push({ id: 'bend:' + n.id, kind: 'bend', dia: Math.max(l1.pipe.d || 0, l2.pipe.d || 0) || null, angle: Math.round(ang * 10) / 10, ...base });
    } else if (k === 1) fixtures.push({ id: 'cap:' + n.id, kind: 'cap', dia: links[0].pipe.d || null, ...base });
  }
  return { pipes, fixtures, cover: C };
}

Take-off of pipes and fittings

64-bit

The count of each fitting by kind and size and the count and total length of pipe by size.

length per size = Σ pipe lengths

Counts are whole numbers; lengths are running 64-bit sums in pipe order. Sizes are grouped by their exact text form, so two sizes that differ in the last digit form two rows.

  • water-objects.js
  • lines 85 to 91
  • CivilCraft 0.8.1
export function schedule(fixtures, pipes, units) {
  const f = new Map(), p = new Map();
  for (const x of fixtures) { const key = x.kind + '|' + (x.dia ?? ''); const r = f.get(key) || { kind: x.kind, size: x.dia ?? null, count: 0 }; r.count++; f.set(key, r); }
  for (const x of pipes) { if (x.kind !== 'pipe') continue; const key = String(x.d ?? ''); const r = p.get(key) || { size: x.d ?? null, count: 0, length: 0 }; r.count++; r.length += x.len; p.set(key, r); }
  const ord = (a, b) => (a.kind || '').localeCompare(b.kind || '') || (a.size ?? 0) - (b.size ?? 0);
  return { fittings: [...f.values()].sort(ord), pipes: [...p.values()].sort((a, b) => (a.size ?? 0) - (b.size ?? 0)) };
}

Take-off as CSV

64-bit

Writes the take-off list to a CSV file.

length = round(length × 100) ÷ 100; size to 1 decimal

The total length per size is rounded to 0.01 for the file, after the unrounded lengths were added. Sizes are written to one decimal.

  • water-objects.js
  • lines 92 to 97
  • CivilCraft 0.8.1
export function scheduleCsv(sched, units, words = (k) => k) {
  const row = (a) => a.map((c) => { const s = String(c ?? ''); return /^[=+\-@]/.test(s) || /[",\r\n]/.test(s) ? '"' + (/^[=+\-@]/.test(s) ? "'" : '') + s.replace(/"/g, '""') + '"' : s; }).join(',');
  return [row([words('wm.sch.what'), words('wm.sch.size'), words('wm.sch.count'), words('wm.sch.length')]),
    ...sched.pipes.map((r) => row([words('wm.sch.pipe'), sizeText(r.size, units), r.count, Math.round(r.length * 100) / 100])),
    ...sched.fittings.map((r) => row([words('wm.fit.' + r.kind), sizeText(r.size, units), r.count, '']))].join('\r\n') + '\r\n';
}
water-ui.js CivilCraft 0.8.1 64-bit 1 calculation Reviewed

Tests None found by the review

Ground elevation of a new node

64-bit

The elevation stored on a junction, reservoir or tank when it is placed on the plan.

z = round(ground × 100) ÷ 100; reservoir head = z + 30 (m) or 100 (ft)

The ground elevation read at the click is rounded to 0.01 with Math.round before it is stored in the model, and a reservoir's first head is that rounded value plus a typed default. The stored elevation is what the water model then uses.

  • water-ui.js
  • lines 70 to 73
  • CivilCraft 0.8.1
const id = P().nextId('wm'), z = ctx.groundAt(w.x, w.y);
edit(t('wm.undo.add.' + kind), (wm) => {
  const n = { id, name: '', x: w.x, y: w.y, z: z == null ? null : Math.round(z * 100) / 100, demand: 0, kind };
  if (kind === 'reservoir') n.head = z == null ? null : Math.round(z * 100) / 100 + (M() ? 30 : 100);

In detail

Method
z = ground − cover; bend angle from two atan2 directions; take-off by counting fittings and adding pipe lengths per size.
Number types
64-bit numbers.
Tolerances
A turn of 5 degrees or more is a bend.
Limits
A pipe's default length is the plan distance, not the sloped length. Fittings are what the layout implies; the file says the list is not a bill of quantities to order from.

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