Resources Numerical accuracy Calculation policy

Duct network pressure: drawing adapter

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.

addons/hvac/lib/ductnet.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests None found by the review

Liters per second in one cfm

64-bit

The factor that converts airflow from cubic feet per minute to liters per second.

1 cfm = 0.3048³ ÷ 60 × 1000 L/s = 0.4719474432 L/s exactly

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.

  • addons/hvac/lib/ductnet.js
  • line 23
  • CadCraft 0.8.2
const turnDeg = (a, b, c) => { const u = [b[0] - a[0], b[1] - a[1]], v = [c[0] - b[0], c[1] - b[1]], lu = Math.hypot(...u), lv = Math.hypot(...v); return lu && lv ? Math.acos(Math.max(-1, Math.min(1, (u[0] * v[0] + u[1] * v[1]) / (lu * lv)))) * 180 / Math.PI : 0; };

Runs handed to the engine

64-bit

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.

  • addons/hvac/lib/ductnet.js
  • lines 67 to 71
  • CadCraft 0.8.2
const e = ends.map((x) => ({ x, d: Math.hypot(x.p[0] - n.p[0], x.p[1] - n.p[1]) })).sort((a, b) => a.d - b.d)[0];
if (!e || e.d > tolMm) continue;
const lps = Number.isFinite(e.x.q) && e.x.q > 0 ? e.x.q * e.x.lps : 0;
if (!lps) notes.push({ code: 'noflow', tag: e.x.tag });
terminals.push({ key: 'end' + terminals.length, node: n.id, lps, label: e.x.tag, p: n.p });

Pascals to inches of water

Exact

Converts a pressure in pascals to inches of water gauge.

in. w.g. = Pa ÷ 249.08891

249.08891 = 0.0254 m × 1000 kg/m³ × 9.80665 m/s², the conventional inch of water, written in full.

  • addons/hvac/lib/ductnet.js
  • line 81
  • CadCraft 0.8.2
export const inWg = (pa) => pa / 249.08891;

Network result CSV

64-bit

Writes the result rows as CSV.

length 2 decimals; airflow 1; velocity 2; rate 3; pressures 1

Every value is written with toFixed, so the file carries the stated decimals: 0.01 m, 0.1 L/s, 0.01 m/s, 0.001 Pa/m and 0.1 Pa.

  • addons/hvac/lib/ductnet.js
  • line 82
  • CadCraft 0.8.2
export const csvRows = (a) => ['run,length_m,airflow_lps,size,velocity_ms,rate_pa_m,friction_pa,fittings_pa,total_pa,status', ...a.rows.map((r) => [r.run, r.lengthM.toFixed(2), r.lps.toFixed(1), r.size, r.V.toFixed(2), r.rate.toFixed(3), r.friction.toFixed(1), r.fittings.toFixed(1), r.dp.toFixed(1), r.status].join(','))].join('\n') + '\n';

In detail

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

Every policy that takes this approach