Resources Numerical accuracy Calculation policy

Cut-list nesting of bars

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.js BimCraft 0.8.1 64-bit 3 calculations Reviewed

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.

Every policy that takes this approach