Resources Numerical accuracy Calculation policy

Numbers in a PartCraft script file

Writes a part's dimensions, positions and parameters as text in a .pcscript file and reads them back.

How the precision is kept

By default every number is written with 12 significant digits and sketch positions with 9 decimals, so a part written to a script and read back can differ from the original beyond those digits. With the exact option numbers are written with String(), which reads back to the same 64-bit number. The reader evaluates literals and expressions in 64-bit and solves the sketches again.

Open item. Write numbers with all their digits by default, as the exact option already does, so a script reads back to the same 64-bit values.

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.

script.js PartCraft 0.8.1 64-bit 3 calculations Reviewed

Tests bin/test-partcraft-params.mjs

Number and position text

64-bit

How a number and a position are turned into text for the script.

number → 12 significant digits; position → 9 decimals, then 12 significant digits

Without the exact option a number is written with toPrecision(12), and a position below 10¹² is first rounded with toFixed(9). The code comment says positions carry the sketch solver's residue and the reader solves them back onto the relations. With exact, String(v) is written and −0 becomes 0.

  • script.js
  • lines 583 to 587
  • PartCraft 0.8.1
const num = (v) => (exact ? String(Object.is(v, -0) ? 0 : v) : formatNumber(v));
// positions (sketch points, picked points) carry the solver's residue (30.0000000000558): written to 9 decimals of
// a millimetre, which the reader solves back onto the relations
const pos = (v) => (exact ? num(v) : formatNumber(Math.abs(v) < 1e12 ? Number(v.toFixed(9)) : v));
const vecS = (a) => a.map(pos).join(', ');

Sketch geometry and dimensions

64-bit

Writes a sketch's points, circles, relations and dimension values.

Point coordinates and circle radii use the 9-decimal position format; dimension values use the 12-digit number format. Sides and fix values are compared with a copy of the sketch rounded the same way, so the file states them only when the reader would measure something else. The fix comparison uses 1e-9 × max(1, |x|).

  • script.js
  • lines 686 to 725
  • PartCraft 0.8.1
function writeSketch(f) {
  out.push(`  on ${planeS(f.plane)}`);   // i18n-ignore: pcscript file text
  const sk = f.sketch;
  // sides and fix values are compared with what the reader will measure: the geometry as written
  const skW = JSON.parse(JSON.stringify(sk));
  for (const e of skW.entities) { if (e.type === 'point') { e.x = Number(pos(e.x)); e.y = Number(pos(e.y)); } if (e.type === 'circle') e.r = Number(pos(e.r)); }
  const labelled = new Set(sk.constraints.filter((c) => c.type === 'offsetlink').map((c) => c.of));
  for (const e of sk.entities) {
    const mods = [];
    if (e.construction) mods.push('construction');
    let body;
    if (e.type === 'point') body = `point ${e.id} ${pos(e.x)}, ${pos(e.y)}`;   // i18n-ignore: pcscript file text
    else if (e.type === 'line') body = `line ${e.id} ${e.p1} ${e.p2}`;   // i18n-ignore: pcscript file text
    else if (e.type === 'circle') body = `circle ${e.id} ${e.c} ${pos(e.r)}`;   // i18n-ignore: pcscript file text
    else if (e.type === 'arc') body = `arc ${e.id} ${e.c} ${e.s} ${e.e}`;   // i18n-ignore: pcscript file text
    else if (e.type === 'spline') {
      body = `spline ${e.id} ${e.kind} ${e.pts.join(' ')}`;   // i18n-ignore: pcscript file text
      if (e.closed) mods.unshift('closed');
      for (const k of ['a0', 'a1']) if (Number.isFinite(e[k])) mods.push(`${k} ${num(e[k])}`);
      if (e.knots) mods.push(`knots ${e.knots.map(num).join(', ')}`);   // i18n-ignore: pcscript file text
    } else body = `# unknown geometry ${e.type}`;
    out.push(`  ${body}${mods.length ? ` [${mods.join('; ')}]` : ''}`);
  }
  for (const c of sk.constraints) {
    const def = S.CONSTRAINTS[c.type];
    let body = `${labelled.has(c.id) ? c.id + ': ' : ''}${c.type} ${c.refs.join(' ')}`;
    const key = f.id + '.' + c.id;
    if (def?.dim) body += links.has(key) ? ` = ${links.get(key)}` : ` ${num(c.value)}`;
    const mods = [];
    let meas = {};
    try { meas = S.measure(skW, c.type, c.refs); } catch { /* unknown: everything is written */ }
    if (c.sign !== meas.sign) mods.push(c.sign === undefined ? 'side none' : `side ${c.sign}`);   // i18n-ignore: pcscript file text
    if (c.kind !== meas.kind) mods.push(c.kind === undefined ? 'kind none' : c.kind);   // i18n-ignore: pcscript file text
    if (c.type === 'fix' && Array.isArray(c.value) && !(Array.isArray(meas.value) && meas.value.length === c.value.length && meas.value.every((x, i) => Math.abs(x - c.value[i]) <= 1e-9 * Math.max(1, Math.abs(x))))) mods.push(`value ${c.value.map(pos).join(', ')}`);   // i18n-ignore: pcscript file text
    if (c.at) mods.push(`at ${vecS(c.at)}`);   // i18n-ignore: pcscript file text
    if (c.off) mods.push(`label ${vecS(c.off)}`);   // i18n-ignore: pcscript file text
    if (c.type === 'offsetlink') mods.push(`of ${c.of}`);   // i18n-ignore: pcscript file text
    out.push(`  ${body}${mods.length ? ` [${mods.join('; ')}]` : ''}`);
  }
}

A feature value read from a script

64-bit

Reads a setting such as a depth, given as a number or as an expression.

A literal is stored as parsed. An expression is evaluated in 64-bit, rounded with Math.round for whole-number settings, checked against the setting's limits, and kept together with its expression as an equation.

  • script.js
  • lines 340 to 353
  • PartCraft 0.8.1
function numberProp(feat, k, v, line, spec) {
  let expr = v.trim(), linked = false;
  if (expr.startsWith('=')) { expr = expr.slice(1).trim(); linked = true; }
  if (!expr) throw new Error(t('xch.script.needs_value', { key: k }));
  const lit = linked ? null : literal(expr);
  if (lit !== null) { feat[k] = lit; return; }
  if (expr === 'none' && !linked) { feat[k] = null; return; }
  let val = calc(expr, k);
  if (spec.integer) val = Math.round(val);
  const vp = valueProblem({ ...spec, label: `${feat.name} ${spec.label}` }, val);
  if (vp) throw new Error(t('xch.script.expr_error', { expr, error: vp }));
  feat[k] = val;
  links.push({ target: feat.id + '.' + k, expr, line });
}

In detail

Method
Numbers pass through formatNumber in expr.js, which uses toPrecision(12). Positions are first rounded with toFixed(9).
Number types
64-bit numbers; decimal text in the file.
Tolerances
A fix relation's stored value is written out only when it differs from the value measured on the written geometry by more than 1e-9 × max(1, |value|).
Limits
12 significant digits by default, fewer than the 15 to 17 a 64-bit number carries; the file's header comment says the same. The call in params-panel.js does not pass the exact option.

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