Resources Numerical accuracy Calculation policy

Export to glTF and STL

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.js CadCraft 0.8.2 32-bit in part 4 calculations Reviewed

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

  • m3d/io.js
  • line 156
  • CadCraft 0.8.2
if (m && !isIdent(m)) node.matrix = m.map((v) => (Math.abs(v) < 1e-15 ? 0 : v));

STL triangles in millimeters

32-bit in part

The triangles of a binary STL file.

x_mm = x × 1000, stored as a 32-bit float

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.

Every policy that takes this approach