Reads and writes STL triangles, with a unit on import and millimeters on export.
How the precision is kept
Binary STL stores 32-bit floats, and both the reader and the writer follow that. ASCII STL is text and can carry more digits, but its coordinates are also put into a Float32Array before import. The unit factor is then applied in 64-bit and the points are kept in a Float64Array.
Open item. Keep ASCII STL coordinates in a Float64Array instead of narrowing them to 32-bit.
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.
io/stl.jsModelCraft 0.8.1 32-bit in part6 calculationsReviewed
Tests tools/modelcraft-test/meshfiles.mjs
Reading a binary file
64-bit
Reads each triangle's nine coordinates and its color word.
The coordinates are 32-bit floats in the file and are kept as such in a Float32Array: the format's own width. A value that is not below 1e9 in size refuses the file. The stored facet normal is not read.
io/stl.js
lines 39 to 67
ModelCraft 0.8.1
function readBinary(b, dv, n) {
if (n > MAX_TRIS) throw new Error(t('stl.too_big'));
let head = '';
for (let i = 0; i < 80; i++) head += String.fromCharCode(b[i]);
const ci = head.indexOf('COLOR=');
const magics = ci >= 0 && ci + 10 <= 80;
const part = magics ? [b[ci + 6], b[ci + 7], b[ci + 8]].map((v) => srgbToLin(v / 255)) : null;
const positions = new Float32Array(n * 9);
const colors = new Float32Array(n * 3);
let any = false;
for (let i = 0; i < n; i++) {
const o = 84 + i * 50;
for (let k = 0; k < 9; k++) {
const v = dv.getFloat32(o + 12 + k * 4, true);
if (!(Math.abs(v) < 1e9)) throw new Error(t('stl.bad', { why: t('stl.why.number') }));
positions[i * 9 + k] = v;
}
const a = dv.getUint16(o + 48, true);
let rgb = null;
if (magics) {
if (!(a & 0x8000)) rgb = [a & 31, (a >> 5) & 31, (a >> 10) & 31];
} else if (a & 0x8000) rgb = [(a >> 10) & 31, (a >> 5) & 31, a & 31];
if (rgb) { any = true; for (let k = 0; k < 3; k++) colors[i * 3 + k] = srgbToLin(rgb[k] / 31); }
else if (part) { any = true; for (let k = 0; k < 3; k++) colors[i * 3 + k] = part[k]; }
else colors.fill(-1, i * 3, i * 3 + 3);
}
const name = head.replace(/\0.*$/s, '').replace(/^solid\s*/i, '').replace(/COLOR=.{4}/s, '').replace(/MATERIAL=.{12}/s, '').trim().slice(0, 80);
return { ascii: false, name, triangles: n, positions, colors: any ? colors : null, convention: any ? (magics ? 'magics' : 'viscam') : null };
}
Reading a text file
32-bit in part
Parses every 'vertex x y z' line.
Each number is parsed to 64-bit and then the whole list is converted with Float32Array.from. A text file written with more than about 7 digits loses the rest here, before the unit factor is applied.
io/stl.js
lines 69 to 83
ModelCraft 0.8.1
function readAscii(b) {
const text = new TextDecoder().decode(b);
const name = (text.match(/^solid[ \t]*([^\r\n]*)/i)?.[1] || '').trim().slice(0, 80);
const re = /vertex\s+(\S+)\s+(\S+)\s+(\S+)/gi;
const pts = [];
let m;
while ((m = re.exec(text))) {
const x = +m[1]; const y = +m[2]; const z = +m[3];
if (![x, y, z].every((v) => Math.abs(v) < 1e9)) throw new Error(t('stl.bad', { why: t('stl.why.number') }));
pts.push(x, y, z);
if (pts.length > MAX_TRIS * 9) throw new Error(t('stl.too_big'));
}
if (!pts.length || pts.length % 9) throw new Error(t('stl.bad', { why: t('stl.why.short') }));
return { ascii: true, name, triangles: pts.length / 9, positions: Float32Array.from(pts), colors: null, convention: null };
}
Unit factors
64-bit
The factor from the file's unit to meters.
mm 0.001; cm 0.01; m 1; inch 0.0254; foot 0.3048
0.0254 and 0.3048 are the exact definitions of the inch and the foot. None of the four decimal factors is an exact binary number, so a product can differ in its last bit from a division by 1000 or 100.
Writes each triangle's normal and corners in millimeters.
n = (b − a) × (c − a), made unit; corner (mm) = world corner (m) × 1000
The normal and the factor of 1000 are 64-bit; the values are then written with setFloat32, the only width binary STL has. A triangle with no area gets a zero normal, because its length falls back to 1.
io/stl.js
lines 148 to 154
ModelCraft 0.8.1
const [a, b, c] = tr.map((x) => x.pos);
const ux = b[0] - a[0]; const uy = b[1] - a[1]; const uz = b[2] - a[2];
const vx = c[0] - a[0]; const vy = c[1] - a[1]; const vz = c[2] - a[2];
let n = [uy * vz - uz * vy, uz * vx - ux * vz, ux * vy - uy * vx];
const l = Math.hypot(...n) || 1;
n = n.map((x) => x / l);
for (const v of [n, a.map((x) => x * 1000), b.map((x) => x * 1000), c.map((x) => x * 1000)]) { dv.setFloat32(o, v[0], true); dv.setFloat32(o + 4, v[1], true); dv.setFloat32(o + 8, v[2], true); o += 12; }
In detail
Method
Triangles welded where their corners are exactly equal; unit factor per coordinate; export in world coordinates times 1000.
Number types
Float32Array for the positions read (binary and ASCII); Float64Array for the imported mesh's points; 32-bit floats in the exported file.
Tolerances
A coordinate must be below 1e9 in size. A triangle whose edge cross product is shorter than 1e-18 (absolute, after the unit factor) is dropped.
Limits
Unit factors: 0.001 (mm), 0.01 (cm), 0.0254 (inch) and 0.3048 (foot); the last two are the exact definitions, and none of the four is an exact binary number. Exported coordinates are 32-bit millimeters: at 100 m from the origin their spacing is about 0.008 mm.
Approach: 32-bit step, flagged
One step stores or exchanges 32-bit values that reach a number a person reads. It is listed here with its open item, and the step is either moved to 64-bit or the app will warn where the number is read.