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.
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)) };
}
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.
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.