Resources Numerical accuracy Calculation policy

3DM import: units, placements and mesh points

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.

io/3dm-import.js ModelCraft 0.8.1 64-bit 4 calculations Reviewed

Tests tools/3dm/test.mjs

Unit factor

64-bit

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.

  • io/3dm-import.js
  • line 63
  • ModelCraft 0.8.1
const k = scene.units.metres > 0 && Number.isFinite(scene.units.metres) ? scene.units.metres : 0.001;

Mesh points in meters

64-bit

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]);
io/3dm/objects.js ModelCraft 0.8.1 64-bit 1 calculation Reviewed

Tests tools/3dm/test.mjs

Mesh record: 32-bit and 64-bit points

64-bit

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.

Every policy that takes this approach