Resources Numerical accuracy Calculation policy

Standard parts and library items

Works out the dimensions, mass and drawing of standard bolts, nuts, washers, pins, keys, bearings and gears, and turns parts and steel sections into library items.

How the precision is kept

Dimensions come from tables and short 64-bit expressions; inch sizes are exact fractions times 25.4. The values that are kept are rounded on purpose: mass to 0.0001 kg, drawing coordinates in a library item to 0.001 mm, and the item's size parameters to 0.1 mm.

Calculations under this policy

9 calculations in 2 source files. 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.

shapes/parts.js CadCraft 0.8.2 64-bit 5 calculations Reviewed

Tests bin/test-shapes.mjs

Part sizes in millimeters

64-bit

The table dimensions of each size of a part family, with inch sizes converted.

mm = in × 25.4; pitch = 25.4 ÷ threads per inch

Inch dimensions are stored as exact binary fractions (7 / 16, 11 / 64) and multiplied by the exact 25.4. The thread pitch is one division.

  • shapes/parts.js
  • lines 128 to 143
  • CadCraft 0.8.2
/** Sizes of a family: [{ size, label, …dims in mm }]. */
export function partSizes(id) {
  switch (id) {
    case 'iso4014': case 'iso4017':
      return METRIC.map(([size, d, P, s, k]) => ({ size, label: size, d, P, s, k }));
    case 'iso4032': return METRIC.map(([size, d, P, s, , m]) => ({ size, label: size, d, P, s, m }));
    case 'iso7089': return METRIC.map(([size, d, , , , , d1, d2, h]) => ({ size: String(d), label: String(d), thread: size, d1, d2, h }));
    case 'asme-hex-bolt': return INCH.map(([size, D, tpi, F, H]) => ({ size, label: size + '-' + tpi + ' UNC', d: D * IN, tpi, P: IN / tpi, s: F * IN, k: H * IN }));
    case 'asme-hex-nut': return INCH.map(([size, D, tpi, F, , Hn]) => ({ size, label: size + '-' + tpi + ' UNC', d: D * IN, tpi, P: IN / tpi, s: F * IN, m: Hn * IN }));
    case 'iso2338': return PINS.map(([d, c]) => ({ size: String(d), label: String(d), d, c }));
    case 'iso15': return BEARINGS.map(([size, d, D, B]) => ({ size, label: size, d, D, B }));
    case 'din6885': return KEYS.map(([b, h, s0, s1, t1, t2]) => ({ size: b + 'x' + h, label: b + ' × ' + h, b, h, shaft: [s0, s1], t1, t2 }));
    case 'spur-gear': return MODULES.map((m) => ({ size: 'm' + m, label: 'm ' + m, m }));
    default: return [];
  }
}

Hexagon area

64-bit

The area of a hexagon from its width across flats.

A = (√3 ÷ 2) × s²

One 64-bit expression with Math.sqrt(3).

  • shapes/parts.js
  • line 167
  • CadCraft 0.8.2
const hexArea = (s) => (Math.sqrt(3) / 2) * s * s;

Mass from volume

64-bit

The mass of a part in kilograms from its volume in mm³.

kg = mm³ × 10⁻⁹ × 7850, rounded to 4 decimals

The result is rounded with toFixed(4) and that rounded number is what the part record keeps, so a mass is stored to the nearest 0.1 g.

  • shapes/parts.js
  • line 168
  • CadCraft 0.8.2
const kg = (mm3) => Number((mm3 * 1e-9 * STEEL_DENSITY).toFixed(4));

Inch length as a fraction

64-bit

Writes an inch length as a whole number and a fraction for the part's designation.

n = round(v × 64); reduce n ÷ 64

The length is rounded to the nearest 1/64 inch with Math.round for the text only; the part's length in millimeters is computed from the unrounded value.

  • shapes/parts.js
  • lines 161 to 166
  • CadCraft 0.8.2
const inchText = (v) => {   // 1.5 → "1-1/2", 0.75 → "3/4" (to 1/64)
  const n = Math.round(v * 64), w = Math.floor(n / 64);
  let num = n - w * 64, den = 64;
  while (num && num % 2 === 0) { num /= 2; den /= 2; }
  return num ? (w ? w + '-' : '') + num + '/' + den : String(w);
};

Part dimensions and mass

64-bit

The full set of dimensions, the bounding box and the mass of one part.

e = 2s ÷ √3; bolt mass = (hex area × k + π ÷ 4 × d² × l) × ρ; gear: d = m·z, d_a = m(z + 2), d_f = m(z − 2.5), d_b = d·cos 20°

64-bit expressions of the table values and the options. The washer and nut volumes subtract the hole from the solid before multiplying by the thickness. Lengths in designations are written with toFixed(3) as text; the stored dimensions are not rounded, the mass is (see Mass from volume).

  • shapes/parts.js
  • lines 170 to 260
  • CadCraft 0.8.2
/** A part record for a family, size and options, or { ok: false, error } (untrusted options: numbers are checked). */
export function makePart(id, size, opts = {}) {
  const fam = findFamily(id);
  if (!fam) return { ok: false, error: 'unknown family' };
  const z = partSizes(id).find((x) => x.size === String(size));
  if (!z) return { ok: false, error: 'unknown size' };
  const o = { ...partOptions(id, z.size) };
  for (const spec of fam.options) {
    if (opts[spec.key] === undefined || opts[spec.key] === null || opts[spec.key] === '') continue;
    const v = Number(opts[spec.key]);
    if (!Number.isFinite(v) || v < spec.min || v > spec.max || (spec.integer && !Number.isInteger(v))) return { ok: false, error: 'bad ' + spec.key, key: spec.key };
    o[spec.key] = v;
  }
  const std = PART_STANDARDS[fam.standard];
  const base = { family: id, type: fam.type, name: fam.name, standard: fam.standard, standardTitle: std.title, url: std.url, size: z.size, units: fam.units, material: fam.material, options: o };
  let dims, designation, sizeText, mass = null, box;
  switch (id) {
    case 'iso4017': case 'iso4014': {
      const l = o.length;
      if (!(l > z.k)) return { ok: false, error: 'bad length', key: 'length' };
      const b = id === 'iso4014' ? (l <= 125 ? 2 * z.d + 6 : l <= 200 ? 2 * z.d + 12 : 2 * z.d + 25) : l;
      if (id === 'iso4014' && !(b < l)) return { ok: false, error: 'bad length', key: 'length' };
      dims = { d: z.d, P: z.P, s: z.s, e: z.s * 2 / Math.sqrt(3), k: z.k, l, b };
      sizeText = z.size + ' × ' + fmt(l);
      designation = fam.standard + ' ' + sizeText;
      mass = kg(hexArea(z.s) * z.k + Math.PI / 4 * z.d * z.d * l);
      box = { w: dims.e, d: z.k + l, h: z.s };
      break;
    }
    case 'asme-hex-bolt': {
      const lin = o.length, l = lin * IN;
      dims = { d: z.d, P: z.P, tpi: z.tpi, s: z.s, e: z.s * 2 / Math.sqrt(3), k: z.k, l, b: Math.min(l, (lin <= 6 ? 2 * z.d / IN + 0.25 : 2 * z.d / IN + 0.5) * IN) };
      sizeText = z.label + ' × ' + inchText(lin) + ' in';
      designation = sizeText + ', ' + fam.standard;
      mass = kg(hexArea(z.s) * z.k + Math.PI / 4 * z.d * z.d * l);
      box = { w: dims.e, d: z.k + l, h: z.s };
      break;
    }
    case 'iso4032': case 'asme-hex-nut': {
      dims = { d: z.d, P: z.P, s: z.s, e: z.s * 2 / Math.sqrt(3), m: z.m, ...(z.tpi ? { tpi: z.tpi } : {}) };
      sizeText = id === 'iso4032' ? z.size : z.label;
      designation = id === 'iso4032' ? fam.standard + ' ' + z.size : z.label + ', ' + fam.standard;
      mass = kg((hexArea(z.s) - Math.PI / 4 * z.d * z.d) * z.m);
      box = { w: dims.e, d: dims.e, h: z.m };
      break;
    }
    case 'iso7089':
      dims = { d1: z.d1, d2: z.d2, h: z.h };
      sizeText = z.size;
      designation = fam.standard + ' ' + z.size;
      mass = kg(Math.PI / 4 * (z.d2 * z.d2 - z.d1 * z.d1) * z.h);
      box = { w: z.d2, d: z.d2, h: z.h };
      break;
    case 'iso2338': {
      const l = o.length;
      if (!(l > 2 * z.c)) return { ok: false, error: 'bad length', key: 'length' };
      dims = { d: z.d, c: z.c, l };
      sizeText = z.size + ' × ' + fmt(l);
      designation = fam.standard + ' ' + sizeText;
      mass = kg(Math.PI / 4 * z.d * z.d * l);
      box = { w: z.d, d: l, h: z.d };
      break;
    }
    case 'iso15':
      dims = { d: z.d, D: z.D, B: z.B };
      sizeText = z.size + ' (' + z.d + ' × ' + z.D + ' × ' + z.B + ')';
      designation = sizeText;
      box = { w: z.D, d: z.D, h: z.B };
      break;
    case 'din6885': {
      const l = o.length;
      if (!(l > z.b)) return { ok: false, error: 'bad length', key: 'length' };
      dims = { b: z.b, h: z.h, l, t1: z.t1, t2: z.t2, shaft: z.shaft };
      sizeText = 'A ' + z.b + ' × ' + z.h + ' × ' + fmt(l);
      designation = fam.standard + ' ' + sizeText;
      mass = kg((z.b * (l - z.b) + Math.PI / 4 * z.b * z.b) * z.h);
      box = { w: l, d: z.b, h: z.h };
      break;
    }
    case 'spur-gear': {
      const m = z.m, teeth = o.teeth, d = m * teeth, da = m * (teeth + 2), df = m * (teeth - 2.5);
      if (!(o.bore < df)) return { ok: false, error: 'bad bore', key: 'bore' };
      dims = { m, z: teeth, d, da, df, db: d * Math.cos(20 * Math.PI / 180), width: o.width, bore: o.bore, alpha: 20 };
      sizeText = 'm ' + fmt(m) + ', z ' + teeth + ', ' + fmt(o.width) + ' wide';
      designation = sizeText + ', ISO 53 / ISO 54';
      box = { w: da, d: da, h: o.width };
      break;
    }
  }
  return { ok: true, part: { ...base, dims, designation, sizeText, massKg: mass, box } };
}
shapes/items.js CadCraft 0.8.2 64-bit 4 calculations Reviewed

Tests bin/test-shapes.mjs

Coordinate rounding in library items

64-bit

Rounds every coordinate written into a library item's drawing.

round(v × 1000) ÷ 1000

Every line, polyline, circle, arc center and hatch point of an item is rounded to 0.001 mm before it is stored. The product v × 1000 is itself rounded to 64-bit before Math.round, so a value almost exactly halfway between two thousandths can go either way.

  • shapes/items.js
  • line 33
  • CadCraft 0.8.2
const r3 = (v) => Math.round(v * 1000) / 1000;

Steel section as a library item

64-bit

The section view or side elevation of a steel section, with its size and weight parameters.

kg/m = published mass, else area × 10⁻⁶ × 7850; weight = kg/m × length ÷ 1000

The outline is taken with 8 segments per arc and its points rounded to 0.001 mm. Width, depth and length parameters are rounded to 0.1 mm, the mass per meter in the text to 0.01 kg/m and the piece weight to 0.01 kg.

  • shapes/items.js
  • lines 78 to 118
  • CadCraft 0.8.2
/** A steel section as a library item: its section view, or an elevation `length` mm long. */
export function sectionItem(sec, { view = 'section', length = 3000 } = {}) {
  const src = SECTION_SOURCES[sec.source];
  const loops = outline(sec, 8);
  const bb = bbox(loops[0]);
  const d = drawer('STEEL');
  const massM = sec.massPub ?? exactArea(sec) * 1e-6 * STEEL_DENSITY;
  const type = FAMILY_TYPE[sec.family] || sec.family;
  const attrs = { [PART_TAGS.type]: type, [PART_TAGS.std]: src.title, [PART_TAGS.size]: sec.name, [PART_TAGS.material]: '' };
  const how = sec.kind === 'I' && sec.r ? '' : sec.kind === 'I' || sec.kind === 'C' ? ' Drawn with parallel flanges and square corners.' : sec.kind === 'L' ? ' Drawn with square corners.' : '';
  if (view === 'elevation') {
    const Lm = Number(length);
    if (!(Lm > 0 && Lm <= 1e6)) throw new Error('length out of range');
    const h = bb.h / 2, e = Math.min(Lm * 0.04, Math.max(10, h * 0.5));
    d.poly([[0, -h], [Lm, -h], [Lm, h], [0, h]]);
    if (sec.kind === 'I' || sec.kind === 'C') for (const s of [1, -1]) d.line(0, s * (h - sec.tf), Lm, s * (h - sec.tf));
    if (sec.kind === 'L') d.line(0, -h + sec.t, Lm, -h + sec.t);
    if (sec.kind === 'RHS' || sec.kind === 'CHS') for (const s of [1, -1]) d.line(0, s * (h - sec.t), Lm, s * (h - sec.t), 'HIDDEN');
    d.line(-e, 0, Lm + e, 0, 'CENTER');
    attrs[PART_TAGS.length] = String(r1(Lm));
    return item({
      category: 'structural/steel',
      name: sec.name + ' elevation, ' + num(Lm) + ' mm',
      description: sec.name + ' (' + type + ') from the ' + src.title + ': ' + secDims(sec) + '; ' + kgm(massM) + ' kg/m. Side elevation, ' + num(Lm) + ' mm long, from the middle of the left end.' + how,
      plan: { layers: layers('STEEL'), entities: d.ents },
      params: { width: r1(Lm), depth: r1(bb.h), weight_kg: Math.round(massM * Lm / 1000 * 100) / 100, remarks: kgm(massM) + ' kg/m' },
      attrs,
    });
  }
  d.fill(loops);
  for (const l of loops) d.poly(l);
  d.cross(0, 0, bb.w / 2 + Math.max(5, bb.w * 0.12), bb.h / 2 + Math.max(5, bb.h * 0.12));
  return item({
      category: 'structural/steel',
    name: sec.name + ' section',
    description: sec.name + ' (' + type + ') from the ' + src.title + ': ' + secDims(sec) + '; ' + kgm(massM) + ' kg/m. Cross-section at its centre.' + how,
    plan: { layers: layers('STEEL'), entities: d.ents },
    params: { width: r1(bb.w), depth: r1(bb.h), remarks: kgm(massM) + ' kg/m' },
    attrs,
  });
}

Bolt drawing dimensions

64-bit

The head, shank, chamfer and thread lines of a bolt's side view.

thread root diameter = D − 1.22687 × pitch; chamfer = min(pitch, D ÷ 4)

The root diameter uses the five-decimal constant 1.22687, and the nut view uses 1.0825 for the minor diameter. These set where the thin thread lines are drawn; they are not used for mass.

  • shapes/items.js
  • lines 121 to 134
  • CadCraft 0.8.2
bolt(d, p) {
  const { d: D, s, e, k, l, b, P: pitch } = p.dims;
  // head across corners, its three visible faces
  d.poly([[-e / 2, 0], [e / 2, 0], [e / 2, k], [-e / 2, k]]);
  for (const x of [-e / 4, e / 4]) d.line(x, 0, x, k);
  // shank with a 45° end chamfer of one pitch
  const c = Math.min(pitch, D / 4);
  d.poly([[-D / 2, 0], [-D / 2, -l + c], [-D / 2 + c, -l], [D / 2 - c, -l], [D / 2, -l + c], [D / 2, 0]], false);
  // ISO 6410: thread root thin, thread limit thick
  const d3 = D - 1.22687 * pitch, yTop = b >= l ? -Math.min(2 * pitch, l / 4) : -(l - b);   // threaded to the head: up to the run-out
  for (const x of [-d3 / 2, d3 / 2]) d.line(x, -l + c, x, yTop, 'THIN');
  if (b < l) d.line(-D / 2, yTop, D / 2, yTop);
  d.line(0, k + 3, 0, -l - 3, 'CENTER');
},

Standard part as a library item

64-bit

The drawing, size parameters and weight of a standard part as a library item.

Width, depth, height and length are rounded to 0.1 mm for the item's parameters and attributes. The weight is the part's mass, already rounded to 0.0001 kg.

  • shapes/items.js
  • lines 195 to 214
  • CadCraft 0.8.2
/** A standard part (parts.js makePart(…).part) as a library item. */
export function partItem(p) {
  const fam = findFamily(p.family);
  const how = DRAW_OF[p.type];
  const d = drawer('PART');
  DRAW[how](d, p);
  const attrs = { [PART_TAGS.type]: p.name, [PART_TAGS.std]: p.standard, [PART_TAGS.size]: p.sizeText, [PART_TAGS.material]: p.material };
  if (p.dims.l) attrs[PART_TAGS.length] = String(r1(p.dims.l));
  const params = { width: r1(p.box.w), depth: r1(p.box.d), height: r1(p.box.h) };
  if (p.massKg) params.weight_kg = p.massKg;
  const ref = p.type === 'gear' ? 'Modules from ISO 54:1996, tooth form from the ISO 53:1998 basic rack.' : 'Dimensions from ' + p.standardTitle.split(' ').slice(0, 2).join(' ') + '.';
  return item({
    category: 'mechanical/parts',
    name: fam.name + ' ' + p.designation,
    description: (fam.name + ', ' + partDims(p) + '. ' + VIEW_TEXT[how] + ' ' + ref + (p.massKg ? ' Weight from the nominal shape in steel (7850 kg/m³), threads not taken off.' : '')).slice(0, 500),
    plan: { layers: layers('PART'), entities: d.ents },
    params,
    attrs,
  });
}

In detail

Method
Table lookups from the standards named in the file; hexagon across corners e = 2s ÷ √3; gear diameters from module and tooth count; mass from the nominal solid at 7850 kg/m³.
Number types
64-bit numbers. 25.4 mm per inch (exact). Inch fractions are written as divisions (7 / 16).
Tolerances
Mass rounded to 4 decimals of a kilogram. Item coordinates rounded to 0.001 mm; width, depth, height and length to 0.1 mm; mass per meter to 0.01 kg/m. Inch lengths shown to 1/64.
Limits
Mass is the nominal shape with threads not taken off, as the item text says. Gear teeth are not drawn.

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