Resources Numerical accuracy Calculation policy

Point cloud positions and read-in values

Turns the points of a scan file into the kept cloud (positions, intensities, size) that the apps measure, slice and snap to.

How the precision is kept

Positions are read and decoded in 64-bit, thinned in Float64Arrays, and then stored as 32-bit offsets from a whole-unit origin. Everything that later reads a point back (pointAt, slices, snapping) therefore works with 32-bit offsets, while the origin and the size of the cloud stay 64-bit.

Open item. Keep the offsets in 64-bit (or split the cloud into tiles with their own origin) where a distance or a position taken from the points has to be better than the 32-bit step.

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.

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

Tests bin/test-pointcloud.mjs

Positions stored as offsets from the origin

32-bit in part

The kept points as 32-bit offsets from a whole-unit origin.

pos = fround(x − floor(min))

The subtraction keep.x[k] - origin[0] is done in 64-bit and the result is stored straight into a Float32Array, which rounds it to the nearest 32-bit value. The origin is rounded down to a whole unit so the offsets stay small, but the stored offset still has about 7 significant digits.

  • pointcloud.js
  • lines 423 to 425
  • CadCraft 0.8.2
const origin = box.min.map((v) => Math.floor(v));
const pos = new Float32Array(n * 3);
for (let k = 0; k < n; k++) { pos[k * 3] = keep.x[k] - origin[0]; pos[k * 3 + 1] = keep.y[k] - origin[1]; pos[k * 3 + 2] = keep.z[k] - origin[2]; }

A point back in file coordinates

32-bit in part

The file-unit position of one kept point.

p = pos + origin

Adds the 64-bit origin to a 32-bit offset in 64-bit arithmetic. The sum is exact for those two numbers but the offset was already rounded to 32 bits when the cloud was stored, so the result carries that rounding.

  • pointcloud.js
  • line 796
  • CadCraft 0.8.2
export const pointAt = (cloud, i) => [cloud.pos[i * 3] + cloud.origin[0], cloud.pos[i * 3 + 1] + cloud.origin[1], cloud.pos[i * 3 + 2] + cloud.origin[2]];

Points in a slice

32-bit in part

The points that lie within a thickness of a plane.

|pos − (at − origin)| ≤ thick ÷ 2

The plane position is turned into an offset in 64-bit (at − origin), then compared with the stored 32-bit offsets against half the thickness with <=. A point within the 32-bit step of the slice boundary can fall on either side.

  • pointcloud.js
  • lines 790 to 795
  • CadCraft 0.8.2
export function sliceOf(cloud, { axis = 'z', at, thick }) {
  const a = AX[axis] ?? 2, c = at - cloud.origin[a], h = Math.abs(thick) / 2;
  const out = [];
  for (let k = 0; k < cloud.n; k++) if (Math.abs(cloud.pos[k * 3 + a] - c) <= h) out.push(k);
  return Uint32Array.from(out);
}

Intensities scaled to 0 to 65535

32-bit in part

The intensity of each kept point on a common 16-bit scale.

out = round((v − lo) ÷ (hi − lo) × 65535)

Values are read into a Float32Array. If all are whole numbers between 0 and 65535 they are copied unchanged (whole numbers below 2^24 are exact in 32-bit). Otherwise they are stretched with Math.round on a 64-bit expression, so the result is a rounded integer; a constant intensity uses span 1.

  • pointcloud.js
  • lines 445 to 454
  • CadCraft 0.8.2
/** Intensities as read → 0–65535: whole numbers in that range stay as they are; others are stretched from lowest to highest. */
function intensities(f, n) {
  let lo = Infinity, hi = -Infinity, whole = true;
  for (let k = 0; k < n; k++) { const v = f[k]; if (v < lo) lo = v; if (v > hi) hi = v; if (whole && v !== Math.round(v)) whole = false; }
  const out = new Uint16Array(n);
  if (whole && lo >= 0 && hi <= 65535) { for (let k = 0; k < n; k++) out[k] = f[k]; return out; }
  const span = hi - lo || 1;
  for (let k = 0; k < n; k++) out[k] = Math.round((f[k] - lo) / span * 65535);
  return out;
}

LAS record decoder

64-bit

The x, y, z of each LAS point from its stored integers.

x = Int32 × scale + offset

Each coordinate is an Int32 times the header scale plus the header offset, computed in 64-bit and passed to Float64Array storage and to the 64-bit bounds. Nothing is rounded here.

  • pointcloud.js
  • lines 456 to 469
  • CadCraft 0.8.2
function recordDecoder(fmt, L, scale, offset) {
  const [sx, sy, sz] = scale, [ox, oy, oz] = offset, ext = fmt >= 6, ca = COLOR_AT[fmt] ?? -1;
  const colorOk = ca >= 0 && ca + 6 <= L;
  return (dv, o, keep, box, classCounts) => {
    const x = dv.getInt32(o, true) * sx + ox, y = dv.getInt32(o + 4, true) * sy + oy, z = dv.getInt32(o + 8, true) * sz + oz;
    const c = ext ? dv.getUint8(o + 16) : dv.getUint8(o + 15) & 31;
    box.add(x, y, z);
    classCounts.set(c, (classCounts.get(c) || 0) + 1);
    if (!keep.want()) return;
    const i = dv.getUint16(o + 12, true);
    if (colorOk) keep.put(x, y, z, dv.getUint16(o + ca, true), dv.getUint16(o + ca + 2, true), dv.getUint16(o + ca + 4, true), i, c);
    else keep.put(x, y, z, 0, 0, 0, i, c);
  };
}

Size of the cloud

64-bit

The extent of the cloud in each axis shown in the summary.

size = max − min

Subtracts the 64-bit max and min of the bounds, which were collected from every point read before thinning, so the size does not depend on the 32-bit positions.

  • pointcloud.js
  • lines 843 to 846
  • CadCraft 0.8.2
export function cloudSummary(cloud) {
  const classes = [...cloud.classCounts.entries()].sort((a, b) => a[0] - b[0]).map(([c, n]) => ({ c, name: className(c) || t('pc.class.other', { c }), n }));
  return { read: cloud.read, kept: cloud.n, size: [0, 1, 2].map((a) => cloud.max[a] - cloud.min[a]), classes };
}

In detail

Method
LAS integer coordinates are scaled and offset in 64-bit; the kept points are stored as Float32Array offsets from origin = floor of the lowest corner of everything read.
Number types
Keeper holds x, y, z in Float64Array; the stored cloud holds pos as Float32Array(3n) relative to origin (64-bit numbers); intensities are read into a Float32Array and finish to Uint16; the bounds (min, max) are 64-bit numbers over every point read, not only the kept ones.
Tolerances
None in these functions; sliceOf compares with <= against half the thickness and the nearest-point search in PlanGrid uses the radius as given.
Limits
A kept position is only as exact as a 32-bit number: for an offset between 512 and 1024 units the step is 2^-14 unit (about 6.1e-5), so a point can move by up to half of that. Larger clouds, measured in a far corner of the box, have a larger step.

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