Resources Numerical accuracy Calculation policy

The model file and what it stores

Keeps members, grids, levels and loose bolts, and reads and writes the model file.

How the precision is kept

The model file is the state written by JSON.stringify, which prints each 64-bit number so that it reads back as the same number. Member points are stored as given, limited to 10 km from the origin. One kind of value is rounded when stored: a loose bolt's size and head to 0.001 mm and its direction to six decimals per component.

Open item. Store a loose bolt's direction as a full 64-bit unit vector instead of six decimals per component.

Calculations under this policy

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

model.js DetailCraft 0.8.1 64-bit 6 calculations Reviewed

Tests bin/test-detailcraft.mjs

Numbers in the model file

64-bit

How the model's numbers are written to the file.

The whole state goes through JSON.stringify with no rounding step, so each number is written with as many digits as it needs to read back identical.

  • model.js
  • lines 215 to 221
  • DetailCraft 0.8.1
toJSON() {
  const text = this.idsText();   // the file carries the IDS text itself; the snapshots only its { name, hash }
  const out = text ? { ...this.state, ids: { ...this.state.ids, text } } : { ...this.state };
  // the pictures carry their PNG in the file (0.8 D8.15); one whose PNG went missing is left out rather than saved blank
  if (this.state.pictures) out.pictures = this.state.pictures.filter((p) => this.pictureSrcs.has(p.hash)).map((p) => ({ ...p, src: this.pictureSrcs.get(p.hash) }));
  return JSON.stringify(out, null, 1);
}

A point of a member

64-bit

Checks and stores a member's start or end point.

Each coordinate goes through Number and must be finite and within 1e7 mm. It is stored unrounded.

  • model.js
  • lines 511 to 516
  • DetailCraft 0.8.1
function vec(p) {
  if (!Array.isArray(p) || p.length !== 3) throw new Error(t('model.err.point'));
  const v = p.map(Number);
  if (v.some((n) => !Number.isFinite(n) || Math.abs(n) > 1e7)) throw new Error(t('model.err.coords'));
  return v;
}

Member rotation and minimum length

64-bit

Brings a member's rotation into 0 to 360 degrees and refuses a member shorter than 1 mm.

rotation = ((r mod 360) + 360) mod 360; length = √(Δx² + Δy² + Δz²) ≥ 1 mm

Two remainders and one addition of 360 bring the angle into range. The length test is Math.hypot against an absolute 1 mm.

  • model.js
  • lines 517 to 526
  • DetailCraft 0.8.1
function checkMember(m, custom) {
  if (!getProfile(m.profile, custom)) throw new Error(t('model.err.profile-unknown', { name: m.profile }));
  if (!MATERIALS.includes(m.material) && !cleanText(m.material, 30)) throw new Error(t('model.err.material'));
  m.material = cleanText(m.material, 30);
  if (!['top', 'middle', 'bottom'].includes(m.position)) throw new Error(t('model.err.position'));
  if (!Number.isFinite(m.rotation)) throw new Error(t('model.err.rotation'));
  m.rotation = ((m.rotation % 360) + 360) % 360;
  const dx = m.end[0] - m.start[0], dy = m.end[1] - m.start[1], dz = m.end[2] - m.start[2];
  if (Math.hypot(dx, dy, dz) < 1) throw new Error(t('model.err.same-points'));
}

Grid lines from spacings

64-bit

The position of each grid line from the list of spacings typed.

at_i = at_{i−1} + spacing_{i−1}

A running sum in the order typed, starting at 0. With whole-millimeter spacings every position is exact; with decimal spacings each addition can round in the last bit.

  • model.js
  • lines 99 to 112
  • DetailCraft 0.8.1
/** Grid lines from labels + spacings ("A B C", "6000 6000"). Returns { x, y } or throws a plain message. */
export function gridsFromSpacings(xLabels, xSpacings, yLabels, ySpacings) {
  const axis = (labels, spacings, which) => {
    const ls = String(labels).split(/[\s,]+/).filter(Boolean);
    const ss = String(spacings).split(/[\s,]+/).filter(Boolean).map(Number);
    if (ls.length > 60) throw new Error(t('model.err.grid-max', { max: 60 }));
    if (ls.length && ss.length !== ls.length - 1) throw new Error(t('model.err.grid-spacings', { which, n: Math.max(0, ls.length - 1) }));
    if (ss.some((v) => !Number.isFinite(v) || v <= 0 || v > 200000)) throw new Error(t('model.err.grid-positive', { which }));
    if (new Set(ls).size !== ls.length) throw new Error(t('model.err.grid-labels', { which }));
    let at = 0;
    return ls.map((label, i) => { if (i) at += ss[i - 1]; return { label: label.slice(0, 8), at }; });
  };
  return { x: axis(xLabels, xSpacings, t('model.grids.numbered')), y: axis(yLabels, ySpacings, t('model.grids.lettered')) };
}

Spacings shown for existing grid lines

64-bit

The spacings text filled into the grid dialog from the stored grid positions.

spacing = (at_i − at_{i−1}) to 3 decimals

The differences are written with toFixed(3). If that text is applied again, the grid lines are rebuilt from spacings rounded to 0.001 mm.

  • model.js
  • lines 113 to 115
  • DetailCraft 0.8.1
export function spacingsOf(lines) {
  return lines.slice(1).map((g, i) => +(g.at - lines[i].at).toFixed(3)).join(' ');
}

A loose bolt as stored

64-bit

The diameter, length, head point and direction kept for a bolt that is not part of a connection.

d, length, head: round(v × 1000) ÷ 1000; axis_i = round(a_i ÷ |a| × 10⁶) ÷ 10⁶

Sizes and the head point are rounded to 0.001 mm. The direction is divided by its length and each component is then rounded to six decimals, so the stored vector's length is 1 only to about six digits; this happens every time a file is read.

  • model.js
  • lines 530 to 540
  • DetailCraft 0.8.1
/** A loose bolt (untrusted): d 3–100, length 5–5000 mm, head anywhere within 10 km, axis any non-zero direction. */
function cleanBolt(b) {
  if (!b || typeof b !== 'object') return null;
  const d = Number(b.d), L = Number(b.length);
  const head = Array.isArray(b.head) ? b.head.map(Number) : [], ax = Array.isArray(b.axis) ? b.axis.map(Number) : [];
  if (!(d >= 3 && d <= 100) || !(L >= 5 && L <= 5000) || head.length !== 3 || ax.length !== 3 || [...head, ...ax].some((v) => !Number.isFinite(v)) || head.some((v) => Math.abs(v) > 1e7)) return null;
  const n = Math.hypot(...ax);
  if (n < 1e-9) return null;
  const r = (v) => Math.round(v * 1000) / 1000;
  return { d: r(d), length: r(L), grade: cleanText(b.grade, 20), kind: b.kind === 'ANCHORBOLT' ? 'ANCHORBOLT' : 'BOLT', head: head.map(r), axis: ax.map((v) => Math.round(v / n * 1e6) / 1e6), phase: cleanPhase(b.phase) || 1 };
}

In detail

Method
Numbers validated and stored; grid lines from a running sum of spacings.
Number types
64-bit numbers; JSON text.
Tolerances
A member must be at least 1 mm long. Coordinates within 1e7 mm. Node names match grids and levels within 0.5 mm.
Limits
A loose bolt's direction is rounded after it is made a unit vector, so it is a unit vector only to about six digits.

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