Brings the meshes, curves and block placements of a .3dm file into the model in meters.
How the precision is kept
The file's unit factor is applied once to every point, in 64-bit, and the points are kept in a Float64Array. Mesh points in a .3dm file are stored as 32-bit floats; newer files also carry 64-bit points, and the reader takes those when they are present and agree with the floats. For a file without them the format sets the accuracy, at about 7 digits per coordinate.
Calculations under this policy
5 calculations in 2 source files. 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.
Chooses the factor that turns the file's unit into meters.
k = meters per file unit, or 0.001 when that is missing, zero or not finite
The factor is the value cc-3dm read from the file's settings. The fallback 0.001 has no exact binary value, so a point multiplied by it can differ in the last bit from the same point divided by 1000.
Scales a mesh's points and builds the model's mesh from them.
p′ = p × k
One multiplication per coordinate, stored in a Float64Array. The source array is whatever the reader produced: 32-bit floats from the file, or the file's 64-bit points. Colors and texture coordinates are copied into the mesh's 32-bit layers.
io/3dm-import.js
lines 31 to 60
ModelCraft 0.8.1
function buildMesh(it, k) {
const P = it.positions, nv = P.length / 3;
const pts = new Float64Array(nv * 3);
for (let i = 0; i < nv * 3; i++) pts[i] = P[i] * k;
const nt = it.indices.length / 3;
const mesh = new Mesh();
const { faceOf, cornerOf, dropped } = mesh._bulk(pts, nv, new Int32Array(nt).fill(3), Int32Array.from(it.indices));
if (it.colors) {
mesh.addAttr('color', 'corner', 4);
const CO = mesh.attrArray('color');
for (let c = 0; c < it.indices.length; c++) { const h = cornerOf[c]; if (h < 0) continue; const v = it.indices[c]; for (let q = 0; q < 4; q++) CO[h * 4 + q] = it.colors[v * 4 + q]; }
}
if (it.uvs) {
mesh.addAttr('uv', 'corner', 2);
const UV = mesh.attrArray('uv');
for (let c = 0; c < it.indices.length; c++) { const h = cornerOf[c]; if (h < 0) continue; const v = it.indices[c]; UV[h * 2] = it.uvs[v * 2]; UV[h * 2 + 1] = it.uvs[v * 2 + 1]; }
}
if (it.color) { const p = mesh.paintIndex(it.color); for (let f = 0; f < nt; f++) if (faceOf[f] >= 0) mesh.fMat[faceOf[f]] = p; }
// smooth inside one face of the file, sharp between faces; a plain mesh by its own normals
if (it.triFace) {
const smooth = new Int32Array(mesh.nf + 1);
for (let f = 0; f < nt; f++) if (faceOf[f] >= 0) smooth[faceOf[f]] = it.triFace[f] + 1;
markSharp(mesh, null, smooth);
} else if (it.normals) {
const N = new Float64Array(mesh.nh * 3);
for (let c = 0; c < it.indices.length; c++) { const h = cornerOf[c]; if (h < 0) continue; const v = it.indices[c]; N[h * 3] = it.normals[v * 3]; N[h * 3 + 1] = it.normals[v * 3 + 1]; N[h * 3 + 2] = it.normals[v * 3 + 2]; }
markSharp(mesh, N);
} else markSharp(mesh, null);
return { mesh, dropped };
}
Block placement matrix
64-bit
Turns a block reference's matrix into the model's placement.
m[c × 4 + r] = x[r × 4 + c]; translation × k
The sixteen numbers are copied in the other order, and only the three translation entries are multiplied by the unit factor. The rotation and scale part is not touched.
io/3dm-import.js
lines 23 to 29
ModelCraft 0.8.1
/** The file's row-major matrix (m[r][c] = x[r * 4 + c]) as a ModelCraft column-major one, with the translation in metres. */
function placement(x, k) {
const m = new Array(16);
for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) m[c * 4 + r] = x[r * 4 + c];
m[12] *= k; m[13] *= k; m[14] *= k;
return m;
}
Curve points in meters
64-bit
Scales each curve's points and drops segments of no length.
a′ = a × k; kept when a′ ≠ b′
A segment is dropped only when its two scaled ends are equal in all three coordinates, an exact comparison with no tolerance.
io/3dm-import.js
lines 128 to 129
ModelCraft 0.8.1
const a = it.pts[i].map((x) => x * k), b = it.pts[i + 1].map((x) => x * k);
if (a[0] !== b[0] || a[1] !== b[1] || a[2] !== b[2]) segs.push([a, b]);
Reads a mesh's points from the file and chooses between its 32-bit and 64-bit copies.
use the 64-bit points when |aᵢ − vᵢ| ≤ |vᵢ| × 10⁻⁶ + 10⁻⁶ × max(1, |aᵢ|) for every coordinate
The format stores points as 32-bit floats; they are read into a Float32Array as they are. Files that also hold 64-bit points have them read into a Float64Array and used instead when every coordinate agrees with its float. If that part of the record cannot be read, the floats are kept, so such a mesh has about 7 digits per coordinate.
io/3dm/objects.js
lines 244 to 307
ModelCraft 0.8.1
function mesh(d, ctx) {
const [major, minor] = d.versioned();
if (major !== 1 && major !== 3) return { cls: 'mesh', unread: true };
const nv = d.i32(), nf = d.i32();
if (nv < 0 || nf < 0 || nv > ctx.limits.items || nf > ctx.limits.items) throw damaged('ccdm.damaged.count');
d.f64s(10);
for (let i = 0; i < 16; i++) d.f32();
const closed = d.i32();
if (d.u8()) d.skipChunk(); // meshing parameters
for (let i = 0; i < 4; i++) if (d.u8()) d.skipChunk(); // curvature statistics
// faces: four corners each (a triangle repeats its third), stored as 1, 2 or 4 byte numbers
const size = d.i32();
if (size !== 1 && size !== 2 && size !== 4 && nf) throw damaged('ccdm.damaged.layout', { tc: 'mesh faces' });
d.count(nf, 4 * size);
const F = new Int32Array(nf * 4);
for (let i = 0; i < nf * 4; i++) F[i] = size === 1 ? d.u8() : size === 2 ? d.u16() : d.i32();
let V = null, N = null, T = null, C = null;
if (major === 1) {
V = arrF32(d, 3); N = arrF32(d, 3); T = arrF32(d, 2);
const nk = d.i32(); if (nk > 0) d.f64s(d.count(nk * 2, 8));
const nc = d.i32(); if (nc > 0) { d.count(nc, 4); C = d.b.slice(d.pos, d.pos + nc * 4); d.pos += nc * 4; }
} else if (nv > 0) {
V = f32view(d.compressed(nv * 12), nv * 3);
N = f32view(d.compressed(nv * 12), nv * 3);
T = f32view(d.compressed(nv * 8), nv * 2);
d.compressed(nv * 16);
C = d.compressed(nv * 4);
}
if (!V || V.length !== nv * 3) { if (nv) throw damaged('ccdm.damaged.layout', { tc: 'mesh points' }); V = new Float32Array(0); }
if (N && N.length !== nv * 3) N = null;
if (T && T.length !== nv * 2) T = null;
if (C && C.length !== nv * 4) C = null;
for (let i = 0; i < F.length; i++) if (F[i] < 0 || F[i] >= nv) throw damaged('ccdm.damaged.index', { what: 'mesh corner' });
// the tail: double-precision points (version 3.7 and later) when the file has them; anything odd here leaves the floats
let dV = null;
try {
if (minor >= 2) d.i32();
if (major === 3 && minor >= 3) {
d.uuid();
if (nv > 0) d.compressed(null);
if (minor >= 4 && ctx.ar.onVersion >= 200606010) {
d.skipChunk();
if (minor >= 5) {
d.u8(); d.u8(); d.u8();
if (minor >= 6) {
if (d.bool()) d.skipChunk();
if (minor >= 7 && d.bool()) {
const { r: q } = d.versionedChunk();
const n = q.u32();
if (n === nv && n > 0) {
const a = f64view(q.compressed(n * 24), n * 3);
// the toolkit drops them when they disagree with the floats; so do we
let ok = !!a;
for (let i = 0; ok && i < n * 3; i++) if (!(Math.abs(a[i] - V[i]) <= Math.abs(V[i]) * 1e-6 + 1e-6 * Math.max(1, Math.abs(a[i])))) ok = false;
if (ok) dV = a;
}
}
}
}
}
}
} catch { dV = null; }
return { cls: 'mesh', nv, nf, V: dV || V, N, T, C, F, closed: closed === 1 || closed === 2 };
}
In detail
Method
point (m) = point (file unit) × meters per file unit; block placements transposed from the file's row order with the translation scaled by the same factor.
Number types
Float32Array or Float64Array as read from the file; Float64Array for the points kept in the model; 64-bit numbers for matrices.
Tolerances
The file's 64-bit points are used only when each coordinate agrees with its 32-bit value within |v| × 1e-6 + 1e-6 × max(1, |a|).
Limits
When the file gives no usable unit factor, 0.001 (millimeters) is assumed. Curved faces arrive as the triangles saved in the file or as cc-3dm's own tessellation.
Approach: 64-bit, stated limit
The arithmetic is 64-bit, but the method or the data sets the accuracy: a solver tolerance, a mesh, a step size, a datum shift, a table. The limit is stated where the result is shown.