Reads and writes Wavefront OBJ files: points, faces, texture coordinates and normals as text, in meters with Y up.
How the precision is kept
On import each number in the text is parsed to a 64-bit number and stored as it is, with the axes swapped. On export every coordinate, texture coordinate and normal is printed with 6 decimals, so an exported point is rounded to 0.000001 m.
Open item. Print exported coordinates with the shortest text that reads back to the same 64-bit number instead of 6 decimals.
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.
Each number is converted from text to the nearest 64-bit number and kept unrounded. A coordinate that is not finite or is 1e7 or more in size refuses the file.
io/obj.js
lines 114 to 117
ModelCraft 0.8.1
if (tag === 'v') {
const x = +parts[1]; const y = +parts[2]; const z = +parts[3];
if (![x, y, z].every((q) => Number.isFinite(q) && Math.abs(q) < 1e7)) throw fail(t('obj.why.vertex', { line: lineNo }));
P.push(x, y, z);
Points into Z up
64-bit
Copies an object's points into the model's coordinates.
(x, y, z) → (x, −z, y)
A swap and a sign change; the points then go into a Float64Array. No unit factor is applied: the file is read as meters.
Marks an edge sharp where the file's normals break across it, where smoothing groups differ, or where the faces bend.
with normals: sharp unless n₁ · n₂ > 0.9995 at both ends; without normals or groups: sharp when the face normals' dot product < 0.99996
0.9995 is the cosine of about 1.8° and 0.99996 of about 0.5°. The corner normals compared are read from the mesh's 32-bit 'normal' layer.
io/obj.js
lines 249 to 263
ModelCraft 0.8.1
/** Sharp where the corner normals break across an edge; with no normals, between smoothing groups (0 = off: all sharp). */
export function markSharp(mesh, N, smoothOf = null) {
const { hTw, hNx, hFl, hF } = mesh;
const has = (a) => N && (N[a * 3] !== 0 || N[a * 3 + 1] !== 0 || N[a * 3 + 2] !== 0);
const same = (a, b) => N[a * 3] * N[b * 3] + N[a * 3 + 1] * N[b * 3 + 1] + N[a * 3 + 2] * N[b * 3 + 2] > 0.9995;
for (let h = 0; h < mesh.nh; h++) {
const g = hTw[h];
if (g < 0 || g < h) continue;
let sharp;
if (has(h) && has(g) && has(hNx[h]) && has(hNx[g])) sharp = !same(h, hNx[g]) || !same(hNx[h], g);
else if (smoothOf) { const a = smoothOf[hF[h]]; const b = smoothOf[hF[g]]; sharp = !a || !b || a !== b; }
else { const na = mesh.faceNormal(hF[h]); const nb = mesh.faceNormal(hF[g]); sharp = na[0] * nb[0] + na[1] * nb[1] + na[2] * nb[2] < 0.99996; }
if (sharp) { hFl[h] |= FL_SHARP; hFl[g] |= FL_SHARP; }
}
}
Number format on export
64-bit
Prints a number for the exported file.
text = the number rounded to 6 decimals, trailing zeros removed
toFixed(6) rounds every exported coordinate to 0.000001 m, and texture coordinates and normals to 6 decimals. This is a rounding of the exported value, not of a display.
Text parsed with the unary plus; (x, y, z) ↔ (x, −z, y); export in world coordinates through each placement's matrix.
Number types
64-bit numbers; Float64Array for imported points.
Tolerances
A vertex coordinate must be finite and below 1e7 in size. Edges are sharp where normals have a dot product of 0.9995 or less, or face normals under 0.99996.
Limits
Exported values are rounded to 6 decimals: a point keeps 1 micrometer, whatever its size.
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.