Resources Numerical accuracy Calculation policy

Manual parts list: quantities and mass

Builds the parts list of an illustrated manual: the quantity, volume and mass of each part, and the CSV export.

How the precision is kept

Volume comes from the part's mesh through meshStats in manual.js. Mass is volume × density ÷ 1000 in 64-bit and the row total is that mass × quantity. Rounding happens only when a cell is written as text: 3 decimals for volume and kilograms, 2 decimals for grams.

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.

illustrate/parts-list.js PartCraft 0.8.1 64-bit 3 calculations Reviewed

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

Quantity and mass per row

64-bit

Counts the placed parts of each row and works out the mass of one and of all.

mass each g = V mm³ × ρ g/cm³ ÷ 1000; total = each × quantity

64-bit throughout. Mass is left empty when the part has no volume or no density. The total is the unrounded unit mass times the count, not a sum of rounded rows.

  • illustrate/parts-list.js
  • lines 57 to 79
  • PartCraft 0.8.1
export function rows(m, meshes, { view = null, scope = 'all', collapse = true } = {}) {
  const shown = scope === 'view' ? new Set(shownInstances(m, view).filter((i) => !view?.insts?.[i.id]?.ghost).map((i) => i.id)) : null;
  const insts = m.instances.filter((i) => !shown || shown.has(i.id));
  const out = [];
  const byKey = new Map();
  for (const inst of insts) {
    const p = m.parts[inst.part];
    let r = null;
    if (collapse) {
      r = byKey.get(inst.part);
      if (!r) r = out.find((x) => x.pids[0] !== inst.part && (p.number ? x.number === p.number : !x.number && x.name === p.name) && sameShape(meshes.get(x.pids[0]), meshes.get(inst.part)));
    }
    if (!r) {
      const vol = statsOf(meshes.get(inst.part)).volume;
      r = { item: numberOf(m, inst.part), qty: 0, pids: [inst.part], insts: [], number: p.number, name: p.name, description: p.description, material: p.material, density: p.density, volume: vol, massEach: vol !== null && p.density ? vol * p.density / 1000 : null, massTotal: null, props: { ...p.props } };
      out.push(r);
    } else if (!r.pids.includes(inst.part)) { r.pids.push(inst.part); if (r.item === null) r.item = numberOf(m, inst.part); }
    if (collapse) byKey.set(inst.part, r);
    r.qty++; r.insts.push(inst.id);
  }
  for (const r of out) r.massTotal = r.massEach === null ? null : r.massEach * r.qty;
  return out.sort((a, b) => (a.item ?? Infinity) - (b.item ?? Infinity) || a.name.localeCompare(b.name));
}

Same shape test

64-bit

Decides whether two parts with the same number or name are one row.

|V₁ − V₂| ≤ 10⁻⁶ × max(V₁, 1) and |A₁ − A₂| ≤ 10⁻⁶ × max(A₁, 1)

The tolerance scales with the first part's volume and area, with a floor of 1. Shapes are compared by vertex count, triangle count, volume and area only.

  • illustrate/parts-list.js
  • lines 50 to 55
  • PartCraft 0.8.1
/** Is the shape of two parts the same (corners and triangles, within a hair)? */
function sameShape(a, b) {
  if (!a || !b || a.positions.length !== b.positions.length || a.indices.length !== b.indices.length) return false;
  const sa = statsOf(a), sb = statsOf(b);
  return Math.abs(sa.volume - sb.volume) <= 1e-6 * Math.max(sa.volume, 1) && Math.abs(sa.area - sb.area) <= 1e-6 * Math.max(sa.area, 1);
}

Mass and volume text

64-bit

The text of the mass and volume cells in the table and the CSV.

volume: 3 decimals; mass ≥ 1000 g: kg with 3 decimals; otherwise g with 2 decimals

toFixed rounds the 64-bit value once and trailing zeros are dropped. The CSV export uses the same text.

  • illustrate/parts-list.js
  • lines 81 to 82
  • PartCraft 0.8.1
const n3 = (v) => (v === null || v === undefined ? '' : String(Number(v.toFixed(3))));
const mass = (g) => (g === null ? '' : g >= 1000 ? String(Number((g / 1000).toFixed(3))) + ' kg' : String(Number(g.toFixed(2))) + ' g');

In detail

Method
Placed parts are grouped into rows by part number or name and shape; mass = volume × density, total = mass × count.
Number types
64-bit numbers; whole-number quantities.
Tolerances
Two parts count as the same shape when their vertex and triangle counts match and their volume and area agree within 1e-6 × max(value, 1).
Limits
The CSV is written from the rounded cell text, not from the 64-bit values.

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