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.jsCadCraft 0.8.2 32-bit in part6 calculationsReviewed
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.
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.
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.
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.