Works out how full a conduit or cable tray is, picks the smallest conduit that holds a set of cables, routes cables through the carriers and lists their lengths.
How the precision is kept
Areas and fill percentages are 64-bit expressions with Math.PI ÷ 4. Conduit inside diameters come from a table in the file given to 0.1 mm, and the fill limits are defaults the person replaces with the code in force, as the file's header says. The cable schedule rounds each length to 0.1 m.
Open item. Keep the unrounded cable length in the schedule row and round to 0.1 m only where it is shown.
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 percentage of a conduit's inside area the cables take, and whether it is within the limit.
pct = 100 × used ÷ (π ÷ 4 × id²); free = area × limit ÷ 100 − used
π ÷ 4 appears in both the cable area and the conduit area and is not cancelled, which costs nothing measurable. The comparison with the limit allows 1e-9. A zero inside diameter gives 0 % instead of a division by zero.
The first trade size, going up, whose fill is within the limit.
Runs conduitFill for each size in trade order with the inside diameter from the table in this file, which is given to 0.1 mm. Returns null when no listed size passes.
addons/electrical/lib/mep/carrier.js
lines 45 to 49
CadCraft 0.8.2
export function conduitFor(type, cables, limits = FILL_DEFAULTS.conduit) {
const T = CONDUIT_TYPES[type] || CONDUIT_TYPES.emt;
for (const size of conduitSizes(type)) { const d = T.sizes[size]; if (conduitFill({ idMm: d.id, cables, limits }).ok) return { size, ...d }; }
return null;
}
Cable tray fill
64-bit
The fill of a tray by area, or by the sum of cable diameters when the cables lie in one layer.
area mode: pct = 100 × used ÷ (width × depth); layer mode: pct = 100 × Σ (od × n) ÷ width
Both modes are one division guarded against a zero width or area. The pass tests allow 1e-9 on the limit, the width and the depth.
addons/electrical/lib/mep/carrier.js
lines 51 to 64
CadCraft 0.8.2
/**
* Tray fill. mode 'area': the cables' cross-sections as a percentage of width × usable depth against `maxPct`; mode 'layer': the cables lie in
* one layer, so the sum of their diameters must fit the width (the single-layer rule for large cables), and the layer height is the biggest cable.
*/
export function trayFill({ widthMm, depthMm, cables, mode = 'area', maxPct = FILL_DEFAULTS.tray }) {
const used = cableArea(cables), area = widthMm * depthMm, count = cableCount(cables);
const biggest = cables.reduce((m, c) => Math.max(m, c.odMm || 0), 0);
if (mode === 'layer') {
const sumOd = cables.reduce((s, c) => s + (c.odMm || 0) * Math.max(1, Math.round(c.n || 1)), 0);
return { mode, count, usedMm2: used, sumOdMm: sumOd, widthMm, pct: widthMm > 0 ? 100 * sumOd / widthMm : 0, limitPct: 100, layerMm: biggest, ok: sumOd <= widthMm + 1e-9 && biggest <= depthMm + 1e-9, tooDeep: biggest > depthMm };
}
const pct = area > 0 ? 100 * used / area : 0;
return { mode, count, usedMm2: used, areaMm2: area, pct, limitPct: maxPct, ok: pct <= maxPct + 1e-9 && biggest <= depthMm + 1e-9, tooDeep: biggest > depthMm, free: area * maxPct / 100 - used };
}
Shortest way through the carriers
64-bit
The shortest path between two nodes of the tray and conduit network, and its length.
Dijkstra: d = dist[u] + segment length × 1000
Each segment's length is multiplied by 1000 as it is added, so the path length is a running sum of products. Equal-length alternatives are decided by rounding and visiting order.
addons/electrical/lib/mep/carrier.js
lines 67 to 85
CadCraft 0.8.2
/**
* Shortest path (by length) between two nodes of the carriers' graph. graph = graphFromRuns(...) over the tray/conduit runs only.
* → { segs: [seg], length (mm) } or null.
*/
function shortest(graph, from, to) {
if (from === to) return { segs: [], length: 0 };
const adj = new Map();
for (const s of graph.segs) for (const [a, b] of [[s.a, s.b], [s.b, s.a]]) { if (!adj.has(a)) adj.set(a, []); adj.get(a).push({ to: b, s }); }
const dist = new Map([[from, 0]]), prev = new Map(), todo = new Set([from]);
while (todo.size) {
let u = null; for (const n of todo) if (u === null || dist.get(n) < dist.get(u)) u = n;
todo.delete(u);
if (u === to) break;
for (const { to: v, s } of adj.get(u) || []) { const d = dist.get(u) + s.length * 1000; if (d < (dist.get(v) ?? Infinity)) { dist.set(v, d); prev.set(v, { u, s }); todo.add(v); } }
}
if (!dist.has(to)) return null;
const segs = []; for (let n = to; prev.has(n); n = prev.get(n).u) segs.unshift(prev.get(n).s);
return { segs, length: dist.get(to) };
}
Cable length through each run
64-bit
For each cable, the runs it passes through and its length in each.
length in a run = Σ segment length × 1000 over that run's segments on the path
A cable with runs fixed by the person takes every segment of those runs; its total is the sum of the per-run sums. Otherwise the total is the path length from the search, and the per-run figures are summed separately from the same segments.
addons/electrical/lib/mep/carrier.js
lines 86 to 107
CadCraft 0.8.2
/**
* routeCables(graph, cables, nodeOf) — cable = { tag, from: key, to: key } (keys the graph was given as points); `fixedRuns` on a cable ([run ids])
* overrides the search (the person said which runs it lies in). → Map tag → { found, lengthMm, runs: [{ run, mm }], segs }
*/
export function routeCables(graph, cables, nodeOf = graph.nodeOf) {
const out = new Map();
for (const c of cables) {
if (c.fixedRuns?.length) {
const segs = graph.segs.filter((s) => c.fixedRuns.includes(s.run));
const runs = c.fixedRuns.map((r) => ({ run: r, mm: segs.filter((s) => s.run === r).reduce((a, s) => a + s.length * 1000, 0) }));
out.set(c.tag, { found: true, lengthMm: runs.reduce((a, r) => a + r.mm, 0), runs, segs: segs.map((s) => s.id) });
continue;
}
const a = nodeOf[c.from], b = nodeOf[c.to];
const p = a != null && b != null ? shortest(graph, a, b) : null;
if (!p) { out.set(c.tag, { found: false, lengthMm: 0, runs: [], segs: [], why: a == null || b == null ? 'ends' : 'gap' }); continue; }
const byRun = new Map();
for (const s of p.segs) byRun.set(s.run, (byRun.get(s.run) || 0) + s.length * 1000);
out.set(c.tag, { found: true, lengthMm: p.length, runs: [...byRun].map(([run, mm]) => ({ run, mm })), segs: p.segs.map((s) => s.id) });
}
return out;
}
Cable schedule length
64-bit
The length of each cable in the schedule, with slack and an allowance at each end.
length m = (route mm × (1 + slack) + 2 × ends mm) ÷ 1000, rounded to 0.1
The value stored in the row and written to the CSV is Math.round(len × 10) ÷ 10, so it is kept to 0.1 m. The defaults are 10 % slack and 1000 mm per end.
addons/electrical/lib/mep/carrier.js
lines 128 to 136
CadCraft 0.8.2
/** Rows of the cable schedule. `slack` (fraction, default 0.1) and `endsMm` (per end, default 1000) are typed allowances, shown as a column of their own. */
export function scheduleRows(cables, routes, { slack = 0.1, endsMm = 1000 } = {}) {
return cables.map((c) => {
const r = routes.get(c.tag) || { found: false, runs: [], lengthMm: 0 };
const len = r.found ? (r.lengthMm * (1 + slack) + 2 * endsMm) / 1000 : 0;
return { tag: c.tag, circuit: c.circuit || '', type: c.type || '', size: c.size || '', odMm: c.odMm || 0, n: c.n || 1, from: c.from || '', to: c.to || '', lengthM: Math.round(len * 10) / 10,
carriers: r.runs.map((x) => x.run).join(' '), note: r.found ? '' : (c.fixedRuns?.length ? '' : r.why === 'ends' ? 'end not on a carrier' : 'no continuous route') };
});
}
Cable list read from CSV
64-bit
Reads cable tags, outside diameters and counts from a CSV file.
The outside diameter and count are read with Number(), so they keep the decimals in the file. A diameter must be above 0 and below 200 mm and a count a whole number from 1 to 1000; other rows are reported as errors.
addons/electrical/lib/mep/carrier.js
lines 140 to 162
CadCraft 0.8.2
/** A cable list typed or exported from the Electrical add-on: tag, circuit, type, size, od (mm), n, from, to. Unknown columns ignored; a bad number is a row error. */
export function parseCablesCsv(text) {
const rows = [], errors = [];
const lines = String(text || '').split(/\r?\n/).filter((l) => l.trim());
if (!lines.length) return { cables: rows, errors };
const split = (l) => { const out = []; let cur = '', q = false; for (let i = 0; i < l.length; i++) { const ch = l[i]; if (q) { if (ch === '"' && l[i + 1] === '"') { cur += '"'; i++; } else if (ch === '"') q = false; else cur += ch; } else if (ch === '"') q = true; else if (ch === ',') { out.push(cur); cur = ''; } else cur += ch; } out.push(cur); return out; };
const head = split(lines[0]).map((h) => h.trim().toLowerCase().replace(/[^a-z]/g, ''));
const col = (n) => head.indexOf(n);
const ix = { tag: col('tag'), circuit: col('circuit'), type: col('type'), size: col('size'), od: Math.max(col('od'), col('odmm')), n: Math.max(col('n'), col('count')), from: col('from'), to: col('to') };
if (ix.tag < 0 || ix.od < 0) return { cables: rows, errors: [{ line: 1, key: 'columns' }] };
const seen = new Set();
for (let i = 1; i < lines.length && i <= 5000; i++) {
const f = split(lines[i]), g = (k) => (ix[k] >= 0 ? (f[ix[k]] ?? '').trim() : '');
const od = Number(g('od')), n = g('n') === '' ? 1 : Number(g('n'));
if (!g('tag')) { errors.push({ line: i + 1, key: 'tag' }); continue; }
if (seen.has(g('tag'))) { errors.push({ line: i + 1, key: 'duplicate', tag: g('tag') }); continue; }
if (!(od > 0 && od < 200)) { errors.push({ line: i + 1, key: 'od', tag: g('tag') }); continue; }
if (!(Number.isInteger(n) && n >= 1 && n <= 1000)) { errors.push({ line: i + 1, key: 'n', tag: g('tag') }); continue; }
seen.add(g('tag'));
rows.push({ tag: g('tag').slice(0, 40), circuit: g('circuit').slice(0, 40), type: g('type').slice(0, 60), size: g('size').slice(0, 30), odMm: od, n, from: g('from').slice(0, 60), to: g('to').slice(0, 60) });
}
return { cables: rows, errors };
}
In detail
Method
Fill % = 100 × Σ (π ÷ 4 × od² × n) ÷ (π ÷ 4 × id²), compared with the limit for 1, 2 or more cables; tray fill against width × depth, or the sum of diameters against the width in single-layer mode; cable route by Dijkstra over the carrier segments; schedule length = route × (1 + slack) + 2 × end allowance.
Number types
64-bit numbers; cable counts rounded to whole numbers of at least 1.
Tolerances
A fill passes when it is within 1e-9 percentage points of the limit; single-layer and depth tests allow 1e-9 mm.
Checked by
tools/bimcraft-test/eg-mep-test.mjs runs BimCraft's mep/carrier.js, which was byte-identical to this file at the review. It checks which limit applies to 1, 2 and more cables, that a cable wider than the conduit is flagged, tray fill by area and by layer, and the schedule rows.
Limits
Conduit sizes are the 19 table entries for EMT and PVC Schedule 40. The default limits (53, 31, 40 and 50 %) are labelled as defaults for review.
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.