The vertex coordinates and placement matrices written when a model is exported as a binary glTF (.glb) or a binary STL file.
How the precision is kept
Both formats store vertex coordinates as 32-bit floats, so the exported coordinates carry about 7 significant digits whatever the model held. glTF keeps each group's vertices in the group's own coordinates and writes its placement matrix as 64-bit JSON numbers, which keeps the 32-bit values small. STL has no placements: every vertex is written in world coordinates, in millimeters.
Open item. Warn when an STL export has coordinates too far from the origin for 32-bit millimeters, or offer to export it about the model's own corner.
Calculations under this policy
4 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.
m3d/io.jsCadCraft 0.8.2 32-bit in part4 calculationsReviewed
TestsNone found by the review
World triangles for export
64-bit
Every triangle of the model in world coordinates, with its normal.
p_world = M · p; n = normalize((p₁ − p₀) × (p₂ − p₀))
Each corner is multiplied by the accumulated placement matrix in 64-bit and the normal is recomputed from the moved triangle's edge vectors. A mirrored placement swaps two corners so the triangle still faces out.
m3d/io.js
lines 75 to 87
CadCraft 0.8.2
/** Triangles of a definition and everything inside it, in the definition's coordinates. */
function flatTris(model, def, m0 = ident()) {
const out = [];
model.walk((d, wm, path, pc) => {
const mirror = det3(wm) < 0;
for (const t of defTris(d, pc)) {
let ps = t.p.map((p) => apply(wm, p));
if (mirror) ps = [ps[0], ps[2], ps[1]];
out.push({ p: ps, n: norm(cross(sub(ps[1], ps[0]), sub(ps[2], ps[0]))), color: t.color });
}
}, def, m0);
return out;
}
glTF vertex positions
32-bit in part
The vertex coordinates and normals stored in the binary part of a .glb file.
Positions and normals are stored into Float32Arrays, in the definition's own coordinates. The accessor's min and max are read back from the 32-bit values, so they match what is stored.
m3d/io.js
lines 130 to 141
CadCraft 0.8.2
const pos = new Float32Array(n * 3), nor = new Float32Array(n * 3), idx = new Uint32Array(n);
const min = [Infinity, Infinity, Infinity], max = [-Infinity, -Infinity, -Infinity];
list.forEach((t, i) => {
for (let k2 = 0; k2 < 3; k2++) {
const o = (i * 3 + k2) * 3, p = t.p[k2];
pos[o] = p[0]; pos[o + 1] = p[1]; pos[o + 2] = p[2];
const q = t.ns ? t.ns[k2] : t.n;
nor[o] = q[0]; nor[o + 1] = q[1]; nor[o + 2] = q[2];
idx[i * 3 + k2] = i * 3 + k2;
}
});
for (let i = 0; i < n; i++) for (let a = 0; a < 3; a++) { const v = pos[i * 3 + a]; if (v < min[a]) min[a] = v; if (v > max[a]) max[a] = v; }
glTF placement matrix
64-bit
The matrix that places each group or component in the .glb file.
The 16 matrix entries go into the JSON part as 64-bit numbers; an entry below 1e-15 in size is written as 0. The matrix is left out when every entry is within 1e-12 of the identity.
World coordinates in meters are multiplied by 1000 (exact factor) in 64-bit and written with setFloat32, as are the normals. At 100 m from the origin the value is 100 000 mm, where 32-bit floats are 2⁻⁷ mm (about 0.008 mm) apart.
m3d/io.js
lines 321 to 333
CadCraft 0.8.2
export function exportSTL(model) {
const tris = flatTris(model, model.root);
const out = new Uint8Array(84 + tris.length * 50);
out.set(enc.encode('ModelCraft STL, millimeters').slice(0, 80)); // i18n-ignore: the STL header
const dv = new DataView(out.buffer);
dv.setUint32(80, tris.length, true);
let o = 84;
for (const t of tris) {
for (const v of [t.n, ...t.p.map((p) => mul(p, 1000))]) { dv.setFloat32(o, v[0], true); dv.setFloat32(o + 4, v[1], true); dv.setFloat32(o + 8, v[2], true); o += 12; }
o += 2;
}
return out;
}
In detail
Method
glTF: one mesh per definition in its own coordinates, one node per placement with its 4×4 matrix, under a root node that turns Z up into Y up. STL: world triangles × 1000.
Number types
Float32Array for glTF positions and normals; DataView.setFloat32 for STL; 64-bit JSON numbers for glTF matrices.
Tolerances
Matrix entries below 1e-15 in size are written as 0. A matrix within 1e-12 of the identity is left out.
Limits
A 32-bit float holds about 7 digits. In STL a vertex 100 m from the origin is 100 000 mm, where neighboring 32-bit values are about 0.008 mm apart.
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.