Resources Numerical accuracy Calculation policy

Manufacturing cost estimate

Estimates what a part costs to machine or print from its volume, bounding box, material price and shop rates.

How the precision is kept

A first-order formula on 64-bit numbers: stock mass times price, removal volume over a removal rate, time per hole, handling and a shared setup. The file's header calls the default rates placeholders, not a quote. Amounts are kept unrounded while they are added and are rounded to two decimals only when returned.

Calculations under this policy

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

cost.js PartCraft 0.8.1 64-bit 5 calculations Reviewed

Tests tools/partcraft-test/test-cost.mjs

Machined part: stock, cutting, holes, handling

64-bit

The cost rows of a part cut from a block of stock.

stock = (size + 2 × allowance) per axis; stock cost = stock volume × density × 1e-9 × price per kg; cutting minutes = (stock volume − volume) ÷ removal rate × finishing; cost = minutes ÷ 60 × rate per hour

The factor 1e-9 converts mm³ × kg/m³ to kg. The removal volume is a difference of the stock and part volumes; the part's volume comes from the build's statistics. Amounts in the rows are unrounded here; the values under the key basis are rounded copies for display.

  • cost.js
  • lines 85 to 95
  • PartCraft 0.8.1
const c = rates.cnc, allow = c.allowance;
const stock = size.map((s) => s + 2 * allow), stockVol = stock[0] * stock[1] * stock[2];
const stockKg = stockVol * density * 1e-9, removal = stockVol - vol, mrr = c.mrr[matId];
if (!(mrr > 0)) throw Object.assign(new Error('There is no removal rate for "' + matId + '".'), { code: 'invalid_argument', next: ['Give rates.cnc.mrr for it.'] });
const holes = holeCount(part);
const cutMin = removal / mrr * c.finishing, holeMin = holes * c.holeMin, minutes = cutMin + holeMin + c.handling;
rows.push({ id: 'stock', what: 'Stock', amount: stockKg * price, basis: { box: stock.map(r2), volume_mm3: r2(stockVol), kg: r2(stockKg * 1000) / 1000, perKg: price } });
rows.push({ id: 'cutting', what: 'Cutting', amount: cutMin / 60 * c.perHour, basis: { removal_mm3: r2(removal), mrr_mm3_min: mrr, finishing: c.finishing, minutes: r2(cutMin) } });
if (holes) rows.push({ id: 'holes', what: 'Holes', amount: holeMin / 60 * c.perHour, basis: { holes, minutesEach: c.holeMin, minutes: r2(holeMin) } });
rows.push({ id: 'handling', what: 'Handling', amount: c.handling / 60 * c.perHour, basis: { minutes: c.handling } });
unit = rows.reduce((a, r) => a + r.amount, 0);

Printed part: filament and machine time

64-bit

The cost rows of a 3D-printed part.

printed volume = volume × fill; filament = printed volume × density × 1e-9 × price per kg; hours = printed volume ÷ volume per hour

Walls plus infill are taken as a fixed share of the solid volume (0.35 by default), which the notes returned with the estimate say. The arithmetic is three products and a quotient.

  • cost.js
  • lines 104 to 106
  • PartCraft 0.8.1
const printed = vol * p.fill, kg = printed * density * 1e-9, hours = printed / p.mm3PerHour;
rows.push({ id: 'material', what: 'Filament', amount: kg * price, basis: { printed_mm3: r2(printed), fill: p.fill, kg: r2(kg * 1000) / 1000, perKg: price } });
rows.push({ id: 'machine', what: 'Machine time', amount: hours * p.perHour, basis: { hours: r2(hours), mm3PerHour: p.mm3PerHour, perHour: p.perHour } });

Cost per part and rounding

64-bit

The cost of each part at the given quantity, the share of each row, and the rounded figures returned.

total = unit + setup ÷ quantity; share = row ÷ total; returned amounts = round to 0.01

The total and the shares are computed from unrounded amounts. Then each row, the unit cost, the setup and the total are rounded to two decimals independently, so the rounded rows need not add up to the rounded total. The part's mass is returned rounded to 1e-5 kg.

  • cost.js
  • lines 113 to 120
  • PartCraft 0.8.1
const total = unit + job / qty;
const shares = {};
for (const r of rows) shares[r.id] = total > 0 ? r.amount / total : 0;
shares.setup = total > 0 ? (job / qty) / total : 0;
rows.push({ id: 'setup', what: 'Setup, shared', amount: job / qty, basis: { job, qty } });
for (const r of rows) r.amount = r2(r.amount);
notes.push('The rates are placeholders for a typical small shop, not a quote: set your own (they are saved with the part).');
return { process, qty, currency: rates.currency, material: matId, partKg: Math.round(partKg * 1e5) / 1e5, volume_mm3: r2(vol), rows, unit: r2(unit), job: r2(job), total: r2(total), perPartAtQty: r2(total), shares, drivers, notes, rates };

Rounding to two decimals

64-bit

Rounds an amount or a listed quantity to hundredths.

r2(v) = round(v × 100) ÷ 100

The product v × 100 is itself rounded in binary before Math.round sees it, so a value that is a half cent in decimal can land on either side. The result is the 64-bit number nearest the two-decimal value.

  • cost.js
  • line 31
  • PartCraft 0.8.1
const r2 = (v) => Math.round(v * 100) / 100;

Number of holes

Exact

Counts the holes the part has, for the time per hole.

hole features + circles in cut sketches, each × (pattern count − 1) more for patterns

Integer counting only. A pattern whose seed is another pattern adds nothing, so holes multiplied by nested patterns are undercounted.

  • cost.js
  • lines 38 to 58
  • PartCraft 0.8.1
/** Holes the part has: hole features (a pattern of them counts each copy) and circles in the sketches of cut extrusions. */
export function holeCount(part) {
  const byId = new Map(part.features.map((f) => [f.id, f]));
  let n = 0;
  const holes = new Map();
  for (const f of part.features) {
    if (f.suppressed) continue;
    if (f.type === 'hole') holes.set(f.id, 1);
    else if (f.type === 'extrude' && f.op === 'cut') {
      const sk = byId.get(f.sketch)?.sketch;
      if (sk) holes.set(f.id, sk.entities.filter((e) => e.type === 'circle' && !e.construction).length);
    }
  }
  const patterned = new Set();
  for (const f of part.features) {
    if (f.suppressed || (f.type !== 'lpattern' && f.type !== 'cpattern')) continue;
    for (const id of f.seeds || []) if (holes.has(id)) { n += holes.get(id) * (Math.max(1, f.count) - 1); }
  }
  for (const v of holes.values()) n += v;
  return n;
}

In detail

Method
CNC: stock = bounding box + allowance on every face, bought by mass; cutting time = (stock volume − part volume) ÷ removal rate × finishing factor. Printed: filament = volume × fill share by mass; time = printed volume ÷ volume rate. Total per part = unit cost + setup ÷ quantity.
Number types
64-bit numbers; money as decimal 64-bit values, not integer cents.
Tolerances
Returned amounts rounded to 0.01; part mass to 0.00001 kg; listed volumes and sizes to 0.01.
Limits
Each row and the total are rounded separately, so the rounded rows can differ from the rounded total by a cent or more. Rounding uses Math.round(v × 100) ÷ 100 on binary values, so an amount exactly on a half cent can round either way.

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