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