Resources Numerical accuracy Calculation policy

Registered scans written as LAS and E57

Moves every point of the registered scans by its scan's matrix and writes the result as a LAS 1.2 or an E57 file.

How the precision is kept

Each point is read from the cloud's position array, which holds 32-bit offsets from the scan's origin. The origin is added and the 4 × 4 registration matrix applied in 64-bit arithmetic. LAS then stores each coordinate as a 32-bit integer multiple of a scale between 0.0001 and 1; E57 stores 64-bit doubles. Either way the exported coordinates start from a 32-bit offset.

Open item. Carry each scan's coordinates in 64-bit from the source file to the writers, so exported LAS and E57 points do not start from 32-bit offsets.

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.

scan-export.js CadCraft 0.8.2 32-bit in part 6 calculations Reviewed

Tests bin/test-register.mjs

Registered position of a point

32-bit in part

One point in the registered frame: its stored position plus the scan's origin, moved by the scan's matrix.

p = M × (pos + origin)

c.pos is the cloud's position array, a Float32Array of offsets from a whole-unit origin (pointcloud.js loadCloud). The origin is added and the matrix applied in 64-bit, but every coordinate written to a file starts from that 32-bit offset, which holds about 7 significant digits.

  • scan-export.js
  • lines 22 to 25
  • CadCraft 0.8.2
const pt = (c, M, i, out) => {
  const x = c.pos[i * 3] + c.origin[0], y = c.pos[i * 3 + 1] + c.origin[1], z = c.pos[i * 3 + 2] + c.origin[2];
  out[0] = M[0] * x + M[1] * y + M[2] * z + M[3]; out[1] = M[4] * x + M[5] * y + M[6] * z + M[7]; out[2] = M[8] * x + M[9] * y + M[10] * z + M[11];
};

Bounds of the registered cloud

32-bit in part

The minimum and maximum x, y, z over every moved point, and the point count.

Each point is moved by pt and compared in 64-bit; the values compared come from the 32-bit positions. With no points both corners are set to 0.

  • scan-export.js
  • lines 26 to 35
  • CadCraft 0.8.2
function bounds(items) {
  const lo = [Infinity, Infinity, Infinity], hi = [-Infinity, -Infinity, -Infinity], p = [0, 0, 0];
  let n = 0;
  for (const { cloud: c, M } of items) for (let i = 0; i < c.n; i++) {
    pt(c, M, i, p); n++;
    for (let a = 0; a < 3; a++) { if (p[a] < lo[a]) lo[a] = p[a]; if (p[a] > hi[a]) hi[a] = p[a]; }
  }
  if (!n) { lo.fill(0); hi.fill(0); }
  return { lo, hi, n };
}

LAS scale and offset

64-bit

Chooses the scale and offset the LAS file stores its integer coordinates with.

scale = first of 0.0001, 0.001, 0.01, 0.1, 1 with extent ÷ scale < 2e9; offset = ⌊min ÷ 100⌋ × 100

The extent is the largest side of the bounds. The offset is each minimum rounded down to a whole hundred, so the stored integers stay small. One scale is used for all three axes.

  • scan-export.js
  • lines 44 to 46
  • CadCraft 0.8.2
const ext = Math.max(hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]);
const scale = [0.0001, 0.001, 0.01, 0.1, 1].find((s) => ext / s < 2e9) ?? 1;
const offset = lo.map((v) => Math.floor(v / 100) * 100);   // whole hundreds: a round offset keeps the stored integers small

LAS point record

32-bit in part

Stores one point's x, y, z as integers in the LAS file.

X = round((p − offset) ÷ scale)

Each coordinate is rounded to the nearest multiple of the scale and written as a signed 32-bit integer, as the LAS format stores it. The header's minimum and maximum are written from the unrounded values, so they can differ from the stored points by up to half a scale step.

  • scan-export.js
  • lines 75 to 76
  • CadCraft 0.8.2
const o = used * rec;
for (let a = 0; a < 3; a++) dv.setInt32(o + a * 4, Math.round((p[a] - offset[a]) / scale), true);

E57 point record

32-bit in part

Stores one point's x, y, z in an E57 data packet and widens the scan's bounds.

The moved coordinates are written as 64-bit doubles with no scaling or rounding. Their source is still the 32-bit position array, so the doubles carry the precision of pt, not of the original scan file.

  • scan-export.js
  • lines 129 to 131
  • CadCraft 0.8.2
const i = i0 + k;
pt(c, M, i, p);
for (let a = 0; a < 3; a++) { dv.setFloat64(starts[a] + k * 8, p[a], true); if (p[a] < lo[a]) lo[a] = p[a]; if (p[a] > hi[a]) hi[a] = p[a]; }

Numbers written as text in the E57 header

64-bit

Writes a bound or a time stamp as text in the E57 XML section.

String(v) gives the shortest text that reads back to the same 64-bit number, so nothing is rounded here. Negative zero is written as 0.

  • scan-export.js
  • line 100
  • CadCraft 0.8.2
const f = (v) => (Object.is(v, -0) ? '0' : String(v));

In detail

Method
p = M × (position + origin), row-major 4 × 4 with the last row left out. LAS: X = round((p − offset) ÷ scale), offset = the minimum rounded down to a whole hundred, scale = the finest of 0.0001, 0.001, 0.01, 0.1, 1 with extent ÷ scale below 2e9. E57 1.0: cartesian x, y, z as doubles in data packets, pages of 1024 bytes with a CRC-32C.
Number types
Float32Array positions (pointcloud.js loadCloud), 64-bit origin and matrix arithmetic, signed 32-bit integers in LAS, 64-bit doubles in E57.
Limits
A 32-bit offset holds about 7 significant digits of the distance from the scan's origin. LAS coordinates are rounded to the chosen scale. Units are the files' own; nothing is converted.

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