Nests the linear parts of a model on stock bars and reports the bars to buy, the length bought and used, the drop of each bar and the waste.
How the precision is kept
Piece lengths are rounded to 0.1 mm before nesting. A bar's need is trim + pieces + one kerf per cut, compared with the stock length plus one kerf, because the last cut needs none when it ends at the bar's end. Used length is the sum of the rounded piece lengths, the drop is rounded to 0.1 mm, and waste is 1 − used ÷ bought.
Calculations under this policy
3 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.
nest.jsBimCraft 0.8.164-bit3 calculationsReviewed
Tests bin/test-detailcraft.mjs
Material a set of cuts needs
64-bit
The bar length a group of cuts takes, with trim and saw kerf.
need = trim + Σ lengths + kerf × cuts
A sum of the piece lengths in order plus one product, in 64-bit.
nest.js
lines 24 to 27
BimCraft 0.8.1
/** Material a set of cuts needs on one bar. */
export function barNeed(cuts, kerf, trim) {
return trim + sum(cuts) + kerf * cuts.length;
}
Nesting pieces on stock bars
64-bit
Which pieces are cut from which bar, each bar's drop, and the bought, used and waste totals.
length = round(length × 10) ÷ 10; fits if need ≤ stock + kerf; drop = stock − trim − used − kerf × cuts; waste = 1 − used ÷ bought
Each piece length is rounded to 0.1 mm before it is nested, and the used total is the sum of those rounded lengths. The drop is rounded to 0.1 mm and cannot be negative. Waste is 0 when nothing is bought.
nest.js
lines 29 to 65
BimCraft 0.8.1
export function nestBars(pieces, { stock = [6000, 12000], kerf = 3, trim = 0, keep = 500 } = {}) {
const byProfile = new Map();
for (const p of pieces) {
if (!(p.length > 0) || !(p.count > 0)) continue;
const k = p.profile.toUpperCase() + '|' + String(p.material).toUpperCase();
if (!byProfile.has(k)) byProfile.set(k, { profile: p.profile, material: p.material, items: [] });
for (let i = 0; i < p.count; i++) byProfile.get(k).items.push({ mark: p.mark, length: Math.round(p.length * 10) / 10 });
}
const groups = [];
for (const g of byProfile.values()) {
const lengths = (Array.isArray(stock) ? stock : stock[g.profile.toUpperCase()] || stock['*'] || [12000]).filter((v) => v > 0).sort((a, b) => a - b);
const max = lengths[lengths.length - 1];
const items = g.items.sort((a, b) => b.length - a.length || a.mark.localeCompare(b.mark, 'en', { numeric: true }));
const bars = [], tooLong = [];
for (const it of items) {
if (barNeed([it.length], kerf, trim) - kerf > max) { tooLong.push(it.mark); continue; }
let bar = bars.find((b) => barNeed([...b.cuts.map((c) => c.length), it.length], kerf, trim) <= b.stock + kerf);
if (!bar) { bar = { stock: max, cuts: [] }; bars.push(bar); } // filled as the longest bar, shrunk below
bar.cuts.push(it);
}
for (const b of bars) {
const need = barNeed(b.cuts.map((c) => c.length), kerf, trim);
b.stock = lengths.find((L) => need <= L + kerf) || b.stock; // the shortest stock length that still takes the cuts
b.used = sum(b.cuts.map((c) => c.length));
const kerfs = kerf * b.cuts.length;
b.drop = Math.max(0, Math.round((b.stock - trim - b.used - kerfs) * 10) / 10);
if (b.drop < 0.05) b.drop = 0; // the last cut ends at the bar's end: no kerf after it
b.scrap = b.drop < keep;
}
const bought = sum(bars.map((b) => b.stock)), used = sum(bars.map((b) => b.used));
groups.push({ profile: g.profile, material: g.material, bars, pieces: items.length - tooLong.length, bought, used, waste: bought ? 1 - used / bought : 0, tooLong,
stock: Object.fromEntries(lengths.map((L) => [L, bars.filter((b) => b.stock === L).length])) });
}
groups.sort((a, b) => a.profile.localeCompare(b.profile, 'en', { numeric: true }));
const bought = sum(groups.map((g) => g.bought)), used = sum(groups.map((g) => g.used));
return { groups, totals: { bars: sum(groups.map((g) => g.bars.length)), bought, used, waste: bought ? 1 - used / bought : 0 } };
}
Cut list tables
64-bit
The summary per profile and the list of bars as printed.
m = mm ÷ 1000, 2 decimals; waste % = waste × 100, 1 decimal
toFixed rounds the printed totals; cut lengths and drops are printed as whole millimeters with Math.round. The result of nestBars keeps the unrounded totals.
nest.js
lines 72 to 87
BimCraft 0.8.1
/**
* The cut list as tables (header row first): summary per profile (pieces, bars to buy by stock length, bought, used,
* waste) with a total row, and every bar with its cuts and drop. Lengths in mm, bought / used in m.
*/
export function cutListTables(res) {
const m = (v) => (v / 1000).toFixed(2), pc = (v) => (v * 100).toFixed(1);
const summary = [['profile', 'material', 'pieces', 'bars', 'bought', 'used', 'waste', 'too-long'].map((k) => t('nest.col.' + k))];
const bars = [['profile', 'bar', 'stock', 'cuts', 'drop', 'keep'].map((k) => t('nest.col.' + k))];
for (const g of res.groups) {
const buy = Object.entries(g.stock).filter(([, n]) => n).sort((a, b) => Number(b[0]) - Number(a[0])).map(([L, n]) => n + ' × ' + L).join(', ');
summary.push([g.profile, g.material, String(g.pieces), buy, m(g.bought), m(g.used), pc(g.waste), g.tooLong.join(' ')]);
g.bars.forEach((b, i) => bars.push([g.profile, String(i + 1), String(b.stock), b.cuts.map((c) => c.mark + ' ' + Math.round(c.length)).join(' + '), String(Math.round(b.drop)), b.scrap ? t('nest.scrap') : t('nest.kept')]));
}
summary.push([t('nest.total'), '', String(sum(res.groups.map((g) => g.pieces))), String(res.totals.bars), m(res.totals.bought), m(res.totals.used), pc(res.totals.waste), '']);
return { summary, bars };
}
In detail
Method
First-fit decreasing for each profile and material; each bar is then shrunk to the shortest stock length that still takes its cuts.
Number types
64-bit numbers, millimeters.
Tolerances
A drop below 0.05 mm is set to 0. A drop shorter than the keep length (500 mm by default) is scrap.
Limits
First-fit decreasing is a heuristic and does not prove the fewest bars. The tables print meters to two decimals, percent to one decimal and cut lengths to whole millimeters.
Approach: 64-bit, direct
Every value is a 64-bit number from input to result. Formulas are closed form or a direct solve, and every comparison uses a stated tolerance.