Turns drawn duct runs into the network the pressure engine analyzes: runs in millimeters, the fan point, the terminals and their airflows, and the result rows.
How the precision is kept
This file converts units and matches points; the pressure calculation is in mep/network.js and mep/flow.js. Coordinates are multiplied by k × 25.4 for inch runs, which is exact, and airflow in cfm by a factor rounded to 9 digits. The fan and the terminals are matched to run ends within a tolerance of 60 mm. The CSV rounds each result to a fixed number of decimals.
Open item. Replace 0.471947443 with 0.4719474432, the exact liters per second in one cubic foot per minute.
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.
The constant is written 0.471947443, which is the exact value rounded to 9 digits; it is about 4e-10 low in relative terms. duct.js uses 0.47194745 for the same factor.
addons/hvac/lib/ductnet.js
line 19
CadCraft 0.8.2
export const CFM_LPS = 0.471947443;
Millimeters per drawing unit
64-bit
The factors that take a run's coordinates, sizes and airflow to millimeters and L/s.
draw = k × 25.4 (inch) or k; size = 25.4 or 1; lps = CFM_LPS or 1
25.4 is exact, so the length factors add one rounding at most. A k that is not positive is replaced by 1.
addons/hvac/lib/ductnet.js
lines 21 to 22
CadCraft 0.8.2
/** mm per drawing unit, mm per size unit, L/s per airflow unit — of one run's data. */
export function mmPer(d) { const sz = d.u === 'in' ? 25.4 : 1; return { draw: (d.k > 0 ? d.k : 1) * sz, size: sz, lps: d.u === 'in' ? CFM_LPS : 1 }; }
Turn at a corner
64-bit
The angle a run turns through at a corner, in degrees.
turn = acos(u·v ÷ (|u| |v|)) × 180 ÷ π
The acos argument is clamped to [−1, 1], and a zero-length side gives 0. For a nearly straight run acos loses digits, because its argument is close to 1; the turn is then itself close to 0.
Each run's vertices in millimeters, its section size in millimeters, and its free ends with their airflow.
vertex mm = p × draw; w mm = w × size; z mm = z × draw
One multiplication per coordinate. The label is built from the size as typed, not from the converted value.
addons/hvac/lib/ductnet.js
lines 25 to 44
CadCraft 0.8.2
export function engineRuns(ducts) {
const runs = [], attrs = {}, ends = [], joints = [];
for (const { id, data: d } of ducts) {
if (!d || !d.pts?.length || d.pts.length < 2 || !d.segs?.length) continue;
const m = mmPer(d), zMm = (Number(d.z) || 0) * m.draw;
let g = 0, from = 0;
for (let i = 1; i <= d.segs.length; i++) {
if (i < d.segs.length && sameSize(d.segs[i], d.segs[from])) continue;
const verts = d.pts.slice(from, i + 1).map((p) => [p[0] * m.draw, p[1] * m.draw, zMm]);
const runId = `${id}.${g++}`, s = d.segs[from];
runs.push({ id: runId, verts });
attrs[runId] = { shape: s.s, wMm: s.w * m.size, hMm: (s.s === 'round' ? s.w : s.h) * m.size, mat: d.mat || 'galv', label: s.s === 'round' ? 'Ø' + s.w : s.w + '×' + s.h };
if (from > 0) joints.push({ run: runId, turn: turnDeg(d.pts[from - 1].map((x) => x * m.draw), d.pts[from].map((x) => x * m.draw), d.pts[from + 1].map((x) => x * m.draw)), at: verts[0] });
if (from === 0) ends.push({ run: runId, p: verts[0], q: d.segs[0].q, lps: m.lps, tag: d.tag || String(id) });
if (i === d.segs.length) ends.push({ run: runId, p: verts[verts.length - 1], q: d.segs[d.segs.length - 1].q, lps: m.lps, tag: d.tag || String(id) });
from = i;
}
}
return { runs, attrs, ends, joints };
}
Fan point matched to a run end
64-bit
Finds the run end the fan sits on and takes its height.
nearest end by √(Δx² + Δy²); accepted when ≤ 60 mm
The fan point is scaled with the factor of the first run that has data, so runs drawn at another scale are not matched in their own units. The tolerance of 60 mm is absolute.
addons/hvac/lib/ductnet.js
lines 49 to 55
CadCraft 0.8.2
const m0 = mmPer(ducts.find((x) => x.data)?.data || { u: 'mm', k: 1 });
const sp = [source.x * m0.draw, source.y * m0.draw, 0];
// the fan sits at the height of the nearest run end it is on
let near = null;
for (const e of ends) { const d = Math.hypot(e.p[0] - sp[0], e.p[1] - sp[1]); if (!near || d < near.d) near = { d, e }; }
if (!near || near.d > tolMm) return { ok: false, why: 'source', distMm: near ? near.d : null };
sp[2] = near.e.p[2];
Terminal airflows
64-bit
The airflow in L/s at each free end of the network.
L/s = q × lps factor of the run
Each end's airflow is the typed value times 1, or times CFM_LPS for an inch run. An end with no airflow is given 0 and noted.
Consecutive sections of one size become one engine run; a change of size adds the turn at that corner as a bend; free ends are terminals with the airflow of the section at that end.
Number types
64-bit numbers.
Tolerances
tolMm = 60 mm for the fan point, the graph and terminal matching; sections are the same size within 1e-9.
Limits
The fan point is converted with the first run's scale. Fitting coefficients are the engine's labelled defaults and static head is ignored, as the file's header says.
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.