Resources Numerical accuracy Calculation policy

Plant equipment solids, capacity and placement

Builds vessels, tanks, columns, pumps and exchangers of the Plant add-on from a few dimensions in millimeters, works out their capacity, and moves them to their place in the model.

How the precision is kept

All arithmetic is 64-bit, in millimeters. Round bodies are meshes with 24 flat sides, so the solid that is drawn and saved is a polygon approximation, while the capacity is computed from the closed-form cylinder and head formulas and not from the mesh. The capacity written to the model is rounded to 0.001 m³ and a nozzle's port size to 0.1 mm.

Open item. Write the capacity property without the 3-decimal rounding and the port size without the 0.1 mm rounding.

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.

addons/plant/lib/equip.js BimCraft 0.8.1 64-bit 7 calculations Reviewed

Tests tools/process-test/test-dec.mjs

Solid of revolution

64-bit

Turns a profile of radius and height points into a closed triangle mesh about the vertical axis.

vertex = (r × cos(2π i ÷ 24), r × sin(2π i ÷ 24), z), i = 0 … 23

Each ring has 24 vertices computed in 64-bit from the profile radius; a radius below 1e-9 becomes a single pole vertex. The body is a 24-sided prism or frustum between rings, not a true round surface.

  • addons/plant/lib/equip.js
  • lines 26 to 39
  • BimCraft 0.8.1
function lathe(profile, sides = N) {   // profile: [[r, z]…] from the axis out and back to the axis; about Z
  const verts = [], tris = [];
  const rings = profile.map(([r, z]) => (r < 1e-9 ? { pole: verts.push([0, 0, z]) - 1 } : { ring: Array.from({ length: sides }, (_, i) => verts.push([r * Math.cos((2 * Math.PI * i) / sides), r * Math.sin((2 * Math.PI * i) / sides), z]) - 1) }));
  for (let k = 0; k + 1 < rings.length; k++) {
    const a = rings[k], b = rings[k + 1];
    for (let i = 0; i < sides; i++) {
      const j = (i + 1) % sides;
      if (a.pole !== undefined && b.ring) tris.push([a.pole, b.ring[i], b.ring[j]]);
      else if (a.ring && b.pole !== undefined) tris.push([a.ring[i], b.pole, a.ring[j]]);
      else if (a.ring && b.ring) { tris.push([a.ring[i], b.ring[i], b.ring[j]]); tris.push([a.ring[i], b.ring[j], a.ring[j]]); }
    }
  }
  return orient({ verts, tris });
}

Elliptical head profile

64-bit

The outline of a 2:1 elliptical vessel head from the tangent line to the pole.

(r × cos a, z₀ ± (r ÷ 2) × sin a), a = (π ÷ 2) × i ÷ 6

Seven points on a quarter ellipse whose depth is a quarter of the diameter, in 64-bit. The six straight steps between them are the only approximation.

  • addons/plant/lib/equip.js
  • lines 55 to 60
  • BimCraft 0.8.1
/** An elliptical head: from the tangent line out to a pole one quarter of the diameter away (2:1), n steps. */
function head(r, z0, up, n = 6) {
  const out = [];
  for (let i = 0; i <= n; i++) { const a = (Math.PI / 2) * (i / n); out.push([r * Math.cos(a), z0 + up * (r / 2) * Math.sin(a)]); }
  return out;
}

Vessel capacity

64-bit

The capacity in cubic meters of a vessel or column: a cylinder and two 2:1 elliptical heads.

V = (π (d ÷ 2)² L + (4 ÷ 3) π (d ÷ 2)² (d ÷ 4)) ÷ 10⁹

One 64-bit expression with Math.PI and the exact factors 4/3 and 1e9; both terms are positive, so nothing cancels. It is the closed-form volume and does not depend on the 24-sided mesh.

  • addons/plant/lib/equip.js
  • line 84
  • BimCraft 0.8.1
const cylCapacity = (d, len) => (Math.PI * (d / 2) ** 2 * len + (4 / 3) * Math.PI * (d / 2) * (d / 2) * (d / 4)) / 1e9;   // m³: a cylinder and two 2:1 heads

Tank capacity

64-bit

The capacity in cubic meters of an atmospheric tank.

V = π r² h ÷ 10⁹

The cylinder up to the shell height in one 64-bit expression. The cone roof drawn above it (height roof) is not counted.

  • addons/plant/lib/equip.js
  • lines 135 to 138
  • BimCraft 0.8.1
mesh: lathe([[0, 0], [r, 0], [r, p.h], [0, p.h + p.roof]]),
nozzles: [nozzle('vent', [0, 0, p.h + p.roof], [0, 0, 1], p.vent, 'SOURCE'), nozzle('inlet', [r, 0, p.h * 0.8], [1, 0, 0], p.inlet, 'SINK'),
  nozzle('outlet', [-r, 0, 250], [-1, 0, 0], p.outlet, 'SOURCE'), nozzle('drain', [0, r, 0], [0, 1, 0], p.drain, 'SOURCE')],
capacityM3: (Math.PI * r * r * p.h) / 1e9,

Signed volume of a mesh

64-bit

The volume enclosed by a triangle mesh in cubic millimeters, positive when the triangles face outward.

V = Σ p · (q × r) ÷ 6 over the triangles

Each triangle adds the volume of the tetrahedron it makes with the origin, summed in order in 64-bit. For a body far from the origin the terms are large and of both signs, so the sum loses digits; inside this file only its sign is used, to turn triangles outward.

  • addons/plant/lib/equip.js
  • lines 63 to 70
  • BimCraft 0.8.1
export function meshVolume(m) {
  let v = 0;
  for (const [a, b, c] of m.tris) {
    const p = m.verts[a], q = m.verts[b], r = m.verts[c];
    v += (p[0] * (q[1] * r[2] - q[2] * r[1]) - p[1] * (q[0] * r[2] - q[2] * r[0]) + p[2] * (q[0] * r[1] - q[1] * r[0])) / 6;
  }
  return v;
}

Dimensions accepted for a piece of equipment

64-bit

Takes the dimensions a person typed, builds the solids and returns their bounding box, nozzles and capacity.

A numeric dimension is used only if it is finite and between 50 and 60000; any other value silently keeps the type's default. Text parameters are cut to 8 characters. The bounding box is the minimum and maximum of the mesh vertices.

  • addons/plant/lib/equip.js
  • lines 198 to 209
  • BimCraft 0.8.1
/** A piece of equipment from its type and dimensions (mm): the solids, their box, the nozzles. Unknown type → null. */
export function makeEquipment(type, params = {}) {
  const def = TYPE[type];
  if (!def) return null;
  const p = { ...def.params };
  for (const k of Object.keys(def.params)) {
    if (params[k] === undefined) continue;
    if (typeof def.params[k] === 'number') { const v = Number(params[k]); if (Number.isFinite(v) && v >= 50 && v <= 60000) p[k] = v; } else p[k] = String(params[k]).slice(0, 8);
  }
  const b = def.build(p);
  return { type, params: p, ifc: def.ifc, mesh: b.mesh, bbox: bboxOf(b.mesh), nozzles: b.nozzles, capacityM3: b.capacityM3 };
}

Equipment moved and turned

64-bit

Moves a piece of equipment to a point and turns it about the vertical axis by an angle in degrees.

(x', y') = (x₀ + x cos a − y sin a, y₀ + x sin a + y cos a), a = rot × π ÷ 180

The angle is converted with Math.PI and every vertex, nozzle point and nozzle direction is rotated in 64-bit. For a turn of 90 degrees the cosine evaluates to about 6e-17 instead of 0, so coordinates that should be equal can differ in the last digits.

  • addons/plant/lib/equip.js
  • lines 211 to 218
  • BimCraft 0.8.1
/** The equipment moved to (x, y, z) and turned `rot` degrees about Z (counterclockwise from above). */
export function placeEquipment(eq, { x = 0, y = 0, z = 0, rot = 0 } = {}) {
  const a = (rot * Math.PI) / 180, c = Math.cos(a), s = Math.sin(a);
  const pt = ([px, py, pz]) => [x + px * c - py * s, y + px * s + py * c, z + pz];
  const dir = ([dx, dy, dz]) => [dx * c - dy * s, dx * s + dy * c, dz];
  const mesh = { verts: eq.mesh.verts.map(pt), tris: eq.mesh.tris };
  return { ...eq, mesh, bbox: bboxOf(mesh), nozzles: eq.nozzles.map((n) => ({ ...n, p: pt(n.p), out: dir(n.out) })) };
}
addons/plant/lib/engineering.js BimCraft 0.8.1 64-bit 2 calculations Reviewed

Tests tools/process-test/test-dec.mjs

Zone solid in millimeters

64-bit

Turns a hazard or consequence zone given in meters into a closed solid in millimeters for the model.

vertex (mm) = vertex (m) × 1000

The zone mesh is requested with 24 sides and each coordinate is multiplied by the exact factor 1000 in 64-bit. The mesh itself is built by process/safety/zones.js, which this review did not cover.

  • addons/plant/lib/engineering.js
  • lines 33 to 37
  • BimCraft 0.8.1
/** A zone volume (metres) as the `equip` placeEquipment takes: a closed solid in millimetres at the site's own coordinates, no nozzles. */
export function zoneEquip(vol) {
  const m = volumeMesh(vol, { sides: 24 });
  return { type: 'zone', params: {}, ifc: { cls: 'IFCBUILDINGELEMENTPROXY', predefined: 'USERDEFINED' }, mesh: { verts: m.verts.map(([x, y, z]) => [x * 1000, y * 1000, z * 1000]), tris: m.tris }, nozzles: [], capacityM3: null, bbox: null };
}

Pipe run handed to the pipe stress input

64-bit

Takes the model's pipe run with the most points and gives its points and total length to the pipe stress calculator.

length = Σ √(Δx² + Δy² + Δz²) over the legs

Coordinates pass through Number() unchanged; a value that is not a number becomes 0. The total length, used as the position of the end anchor, is a sum of Math.hypot leg lengths added in order.

  • addons/plant/lib/engineering.js
  • lines 38 to 45
  • BimCraft 0.8.1
/** A pipe run of the model ({ points mm, size, tag }) → the pipe stress input fields to merge: points (3-D, mm) and size; the rest stays the person's. */
export function pickRun(runs) {
  const r = (runs || []).filter((x) => x && Array.isArray(x.points) && x.points.length >= 2 && x.kind !== 'duct').sort((a, b) => b.points.length - a.points.length)[0];
  if (!r) return null;
  const pts = r.points.map((p) => [Number(p[0]) || 0, Number(p[1]) || 0, Number(p[2]) || 0]);
  const len = pts.reduce((s, p, i) => (i ? s + Math.hypot(p[0] - pts[i - 1][0], p[1] - pts[i - 1][1], p[2] - pts[i - 1][2]) : 0), 0);
  return { tag: r.tag || r.guid || 'run', merge: { points: pts, size: String(r.size || '4'), supports: [{ s: 0, type: 'anchor' }, { s: len, type: 'anchor' }] } };
}
addons/plant/lib/place.js BimCraft 0.8.1 64-bit 2 calculations Reviewed

Tests None found by the review

Nozzle port size

64-bit

The outside diameter in millimeters stored on a nozzle's port, from its nominal pipe size.

size = round(od (in) × 25.4 × 10) ÷ 10

The outside diameter in inches comes from the steel schedule 40 pipe table, is multiplied by the exact 25.4 and is rounded to 0.1 mm before it is stored. A size that is not in the table is read as a number of inches, and a result of 0 is replaced by 50.

  • addons/plant/lib/place.js
  • lines 23 to 25
  • BimCraft 0.8.1
/** A nozzle as the port shape addPorts takes (the element's own axes, mm): nominal = its NPS label, size = the outside diameter. */
export const portOf = (nz) => ({ name: nz.name, system: '', kind: 'pipe', direction: nz.flow === 'SINK' ? 'in' : nz.flow === 'SOURCE' ? 'out' : 'both', shape: 'round',
  size: Math.round((pipeSize('steel', '40', String(nz.size))?.od ?? Number(nz.size) ?? 0) * 25.4 * 10) / 10 || 50, nominal: String(nz.size), x: nz.p[0], y: nz.p[1], z: nz.p[2], axis: nz.out });

Capacity property in the model

64-bit

The capacity in cubic meters written to the CadCraft_Plant property set of the placed equipment.

CapacityM3 = round(V × 1000) ÷ 1000

The 64-bit capacity is rounded to 3 decimals of a cubic meter (1 liter) before it is stored as an IFCREAL. A capacity of 0 or null is not written.

  • addons/plant/lib/place.js
  • line 72
  • BimCraft 0.8.1
if (eq.capacityM3) rows.push(['CapacityM3', 'IFCREAL', Math.round(eq.capacityM3 * 1000) / 1000]);

In detail

Method
Revolution of a profile about Z into 24-sided rings; 2:1 elliptical heads in 6 steps; capacity = cylinder + ellipsoid; placement by a turn about Z and a shift.
Number types
64-bit numbers in plain arrays of [x, y, z].
Tolerances
A profile point with radius below 1e-9 mm is a pole. A numeric dimension is accepted only from 50 to 60000 mm; outside that range the type's default is used.
Limits
A 24-sided ring has (24 ÷ 2π) × sin(2π ÷ 24) = 0.9886 of the area of the circle it stands for, so the mesh of a round body encloses about 1.1 % less volume than the capacity that is stated. The tank capacity leaves the cone roof out.

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