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