Resources Numerical accuracy Calculation policy

E57 point cloud reading

Reads the points of an E57 scan file into absolute coordinates in meters, with each scan's pose applied, and the bounds of every point read.

How the precision is kept

Coordinates are decoded into 64-bit numbers and kept in a Float64Array. Their accuracy is set by how the file stores them: a 64-bit float, a 32-bit float widened to 64-bit, or a scaled integer (raw + minimum) × scale + offset. Each scan's rotation (a unit quaternion) and translation are applied in 64-bit. When the file has more points than the budget, points are thinned evenly; the bounds are taken over every point read, kept or not.

Calculations under this policy

8 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.

e57.js CadCraft 0.8.2 64-bit 8 calculations Reviewed

Tests bin/test-pointcloud.mjs

Point in absolute coordinates

64-bit

One scan point, from Cartesian or spherical fields, moved by the scan's pose.

spherical: x = r cos e cos a, y = r cos e sin a, z = r sin e; then p′ = R × p + T

The rotation is a sum of three products per coordinate and the translation is added last, so a scan far from the origin adds a large offset to small local coordinates in ordinary 64-bit arithmetic. Points flagged invalid or not finite are skipped and counted.

  • e57.js
  • lines 364 to 369
  • CadCraft 0.8.2
if (cart) { x = C.x[k]; y = C.y[k]; z = C.z[k]; }
else { const rr = C.r[k], ca = Math.cos(C.e[k]); x = rr * ca * Math.cos(C.a[k]); y = rr * ca * Math.sin(C.a[k]); z = rr * Math.sin(C.e[k]); }
if (C.bad && C.bad[k] !== 0) continue;
if (!(Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z))) continue;
if (R) { const X = R[0] * x + R[1] * y + R[2] * z + T[0], Y = R[3] * x + R[4] * y + R[5] * z + T[1], Z = R[6] * x + R[7] * y + R[8] * z + T[2]; x = X; y = Y; z = Z; }
else { x += T[0]; y += T[1]; z += T[2]; }

Rotation matrix from the pose quaternion

64-bit

The 3 × 3 rotation of a scan from its quaternion (w, x, y, z).

q ÷ |q|; R = [1 − 2(y² + z²), 2(xy − zw), 2(xz + yw); 2(xy + zw), 1 − 2(x² + z²), 2(yz − xw); 2(xz − yw), 2(yz + xw), 1 − 2(x² + y²)]

The quaternion is normalized with Math.hypot first, so a file whose quaternion is slightly off unit length still gives a rotation. A quaternion of all zeros divides by 1 and gives the identity rotation, not an error.

  • e57.js
  • lines 299 to 304
  • CadCraft 0.8.2
const R = (() => {
  if (!s.pose) return null;
  let [w, x, y, z] = s.pose.rotation;
  const n = Math.hypot(w, x, y, z) || 1; w /= n; x /= n; y /= n; z /= n;
  return [1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w), 2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w), 2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y)];
})();

Field values of a packet

64-bit

Decodes one field of a data packet into numbers: 64-bit floats, 32-bit floats or scaled integers.

float: as stored; integer: (raw + minimum) × scale + offset

The result array is a Float64Array. A field stored as a 32-bit float is read with getFloat32 and widened, so it keeps the file's 32-bit precision. A scaled integer is one addition, one multiplication and one addition in 64-bit; the scale is a decimal from the file's XML and is the nearest 64-bit number to it.

  • e57.js
  • lines 348 to 359
  • CadCraft 0.8.2
const col = (j) => {
  const q = s.fields[j];
  const out = new Float64Array(n);
  if (!want[j]) return out;
  const buf = bufs[j], b0 = boff[j];
  if (q.kind === 'f64' || q.kind === 'f32') {
    const d = new DataView(buf.buffer, buf.byteOffset, buf.byteLength), w = q.kind === 'f64' ? 8 : 4, o0 = b0 >>> 3;
    for (let k = 0; k < n; k++) out[k] = q.kind === 'f64' ? d.getFloat64(o0 + k * w, true) : d.getFloat32(o0 + k * w, true);
  } else if (q.bits === 0) out.fill(q.min * q.scale + q.offset);
  else for (let k = 0; k < n; k++) out[k] = (readInt(buf, b0 + k * q.bits, q.bits) + q.min) * q.scale + q.offset;
  return out;
};

Bit-packed integer

64-bit

Reads an unsigned integer of up to 64 bits from a bit position in a packet.

value = Σ chunkᵢ × 2^(32 i), chunks of up to 32 bits

Up to 32 bits are assembled with bitwise operators and made unsigned; wider values are summed as 64-bit numbers. The sum is exact below 2⁵³; a wider raw value loses its lowest bits. The bitwise operators here work on packed file bits, not on a measured value.

  • e57.js
  • lines 265 to 270
  • CadCraft 0.8.2
function readInt(buf, pos, bits) {
  if (bits <= 32) return bits24(buf, pos, bits);
  let r = 0, got = 0;
  while (got < bits) { const take = Math.min(32, bits - got); r += bits24(buf, pos + got, take) * 2 ** got; got += take; }
  return r;
}

Bits from the stream

64-bit

Extracts up to 32 bits starting at any bit of a byte buffer, least significant bit first.

Four bytes are combined and shifted; when the start is not on a byte boundary a fifth byte supplies the top bits. Every step is forced unsigned with >>> 0. This is integer bit handling and is exact.

  • e57.js
  • lines 258 to 264
  • CadCraft 0.8.2
/** `bits` (≤ 24) bits at bit position `pos` of `buf`, least significant bit first. */
function bits24(buf, pos, bits) {
  const i = pos >>> 3, sh = pos & 7;
  let v = ((buf[i] | (buf[i + 1] << 8) | (buf[i + 2] << 16) | (buf[i + 3] << 24)) >>> sh) >>> 0;
  if (sh) v = (v | ((buf[i + 4] || 0) << (32 - sh))) >>> 0;   // bits 32.. of the window (a shift by 32 would not shift)
  return bits === 32 ? v : v & (2 ** bits - 1);
}

Field description from the XML

64-bit

How many bits an integer field uses, and its minimum, scale and offset.

bits = smallest b with 2ᵇ > maximum − minimum (at most 64)

Minimum, maximum, scale and offset are parsed from the file's decimal text with Number. The bit count compares 2ᵇ with maximum − minimum in 64-bit, which is exact for ranges below 2⁵³. The defaults for a missing range are −2⁶³ and 2⁶³ − 1, the second of which rounds to 2⁶³ in 64-bit.

  • e57.js
  • lines 208 to 219
  • CadCraft 0.8.2
const field = (k) => {
  const a = k.attrs;
  if (k.name === 'cartesianInvalidState' || k.name === 'sphericalInvalidState') { /* an Integer 0..2 like any other */ }
  if (k.attrs.type === 'Float') return { name: k.name, kind: a.precision === 'single' ? 'f32' : 'f64' };
  if (k.attrs.type === 'Integer' || k.attrs.type === 'ScaledInteger') {
    const min = nu(a.minimum, -(2 ** 63)), max = nu(a.maximum, 2 ** 63 - 1);
    let bits = 0;
    while (2 ** bits <= max - min && bits < 64) bits++;
    return { name: k.name, kind: 'int', bits, min, scale: k.attrs.type === 'ScaledInteger' ? nu(a.scale, 1) : 1, offset: k.attrs.type === 'ScaledInteger' ? nu(a.offset, 0) : 0 };
  }
  throw fail('unsupported', { detail: 'field type ' + k.attrs.type });   // i18n-ignore: technical {detail}
};

Thinning to the point budget

64-bit

How many points are kept and at what rate when a file has more points than the budget.

cap = max(1, min(listed, ⌊budget⌋)); rate = min(1, cap ÷ listed); keep a point each time the running sum of rate reaches 1

The rate is added once per point read; the rounding of that running sum can shift which points are kept, not their coordinates. The bounds (minimum and maximum) are updated before the thinning, so they cover every point read.

  • e57.js
  • lines 119 to 120
  • CadCraft 0.8.2
const cap = Math.max(1, Math.min(listed, Math.floor(budget)));
const rate = Math.min(1, cap / listed);

Points from the library's text

64-bit

Parses the x y z lines the web-e57 library returns and keeps the bounds and the thinned points.

x, y, z = Number of each field of a line

Each coordinate is converted from decimal text to the nearest 64-bit number. The digits available are those the library wrote; this code does not control them. Lines with a value that is not finite are skipped.

  • e57.js
  • lines 131 to 141
  • CadCraft 0.8.2
const v = text.slice(pos, end).split(' ');
const x = +v[0], y = +v[1], z = +v[2];
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
  read++;
  if (x < min[0]) min[0] = x; if (x > max[0]) max[0] = x;
  if (y < min[1]) min[1] = y; if (y > max[1]) max[1] = y;
  if (z < min[2]) min[2] = z; if (z > max[2]) max[2] = z;
  acc += rate;
  if (acc >= 1 && kept < cap) {
    acc -= 1;
    xyz[kept * 3] = x; xyz[kept * 3 + 1] = y; xyz[kept * 3 + 2] = z;

In detail

Method
The app's own reader of the ASTM E2807 layout: paged file, XML section, bit-packed integer and float fields. Files it refuses as unsupported go to the web-e57 1.2.0 library, whose answer is decimal text that is parsed here.
Number types
Coordinates and bounds: 64-bit (Float64Array). Color: 8-bit. Intensity: a Float32Array, which the app's own reader fills with whole numbers from 0 to 255.
Limits
A field the file stores as a 32-bit float carries about 7 significant digits. An integer field wider than 53 bits cannot be held exactly in a 64-bit number. Page checksums are not verified. In the web-e57 path the number of digits is whatever that library prints.

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