@voxelkloud/wasm-build
v0.5.1
Published
Build a streamable octree in the browser from a LAS, LAZ, E57, PLY, PCD or XYZ file with no index — the voxelkloud converter's own partitioner, compiled to wasm.
Maintainers
Readme
@voxelkloud/wasm-build
Build a streamable octree in the browser, from a file that has none: LAS, LAZ, E57, PLY, PCD or XYZ — and write one back out as a COPC. Part of voxelkloud. 565 KB, 206 KB gzipped — fetched when a build actually starts, not when the page loads.
E57 is most of that weight: its reader and the XML parser under it take a 338
KB / 122 KB module to 565 KB / 206 KB. Worth it because it is the
SAME reader voxelkloud convert runs — a scan indexed in a tab and the same
scan indexed on a workstation are one implementation, not two that agree until
they do not.
npm install @voxelkloud/wasm-buildexamples/convert.html is a drop zone in one file — serve this directory
(voxelkloud serve packages/wasm-build) and open it.
import { buildFromFile, builtNodes, initBuilder } from "@voxelkloud/wasm-build";
await initBuilder();
const cloud = buildFromFile(new Uint8Array(await file.arrayBuffer()));
cloud.pointCount; // 341989
cloud.nodeCount; // 27
cloud.rootSpacing; // 0.044414062500000004
builtNodes(cloud); // [{ index, key: "0-0-0-0", level, x, y, z, pointCount }, ...]
cloud.nodeRecords(0); // raw LAS point records for that nodeConverting
The other direction, and the reason the writer is in here: a file that has no index goes in, and one that streams comes out — on a machine that never sent the scan anywhere. In AEC that is not a convenience. A client's survey often cannot leave the building, which makes every hosted converter unusable regardless of what it costs.
import { convertFileToCopc, initBuilder } from "@voxelkloud/wasm-build";
await initBuilder();
const copc = convertFileToCopc(new Uint8Array(await file.arrayBuffer()));
// Hand it to the user, or to the File System Access API.
const url = URL.createObjectURL(new Blob([copc]));The formats it reads
Five, sniffed from the bytes — nothing is passed in to say which is which:
| | how it is recognised | what is read |
| --- | --- | --- |
| LAS | LASF | every point format, 0 through 10 |
| LAZ | LASF + a laszip encoded VLR | the same, decompressed by laz-rs |
| PLY | ply | ascii and both binary endiannesses; the vertex element only |
| PCD | # .PCD or VERSION | ascii, binary, and binary_compressed (LZF, struct-of-arrays) |
| XYZ | the first line parses as numbers | whitespace, comma or semicolon; 3, 4, 6 or 7 columns |
The last four have no header worth the name and no projection, so what comes out
of them is quantised into LAS records: point format 0, or 2 when the file
carried colour. Scale is the coarsest power of ten that still resolves the
extent into an i32, floored at a millimetre, and the offset is the extent's
minimum — the same choices voxelkloud convert makes writing a LAS.
Two details that are easy to get wrong and are tested: 8-bit colour widens to
16-bit by ×257, so 255 reaches 65535 and not 65280; and PCD's rgb field is
a float whose bit pattern is three bytes, so reading it as a number gives
nonsense.
E57 is not here. It is an XML document plus binary sections with their own compression, which is a driver rather than a parser.
It is not a second implementation
The partitioner is
voxelkloud-io::build — the same function
voxelkloud convert calls, compiled to a different target. A browser-only
octree builder would be a third spelling of a rule that already has two
consumers, and the two would agree right up until they did not.
The evidence that it holds: on lion_takanawa, this produces a root spacing of
0.044414062500000004 over a cube of -4.985 .. 0.700 — bit-identical to
what PotreeConverter derived for the same cloud, years before this code existed.
That is in the test suite, and it is what makes a tree built in a page
comparable to one built by anything else in the ecosystem.
What it costs, measured
A 20M-point LAS 1.2 tile — 28-byte records, 134 MB compressed:
| | | | --- | --- | | build | 31 seconds | | peak memory | 1.7 GB | | of which the partition | a small part; the laszip decode is most of it |
Both scale linearly, which is why MAX_POINTS is 20 million and not the 30
the address space would technically allow. A tab that asks for 3.5 GB is a tab
that gets killed. Past the limit the error names voxelkloud convert, because
that is the actual answer.
Those 31 seconds are synchronous. Nothing on the JS side runs during a
build — no timer fires, no promise resumes — so anything above a few million
points belongs in a worker. @voxelkloud/format-single
ships that worker and uses it by default wherever Worker exists.
For the same reason progress is pushed, not polled:
onBuildProgress((points) => postMessage({ points }));
const cloud = buildFromFile(bytes);
onBuildProgress(undefined);The callback fires roughly every 100,000 points, on the thread running the
build, from inside the decode loop. A setInterval beside a build would not
fire once before it returned — the bar would sit at zero and then jump to done.
buildProgress() returns the last count reported and is only useful afterwards,
or to a caller driving the build itself.
What comes out
Raw LAS point records, per node — the same thing a COPC chunk decodes to. That
is deliberate: @voxelkloud/format-las already turns those
into attributes for the COPC and EPT drivers, so this tier reuses that decoder
instead of growing a second one. The builder also carries the LASF_Projection
and Extra Bytes VLR payloads across the boundary untouched, so the CRS and the
custom dimensions are read by the same parsers too.
The driver that puts all of it together is
@voxelkloud/format-single.
Building from source
Needs cargo, rustup target add wasm32-unknown-unknown, and
cargo install wasm-bindgen-cli --version 0.2.127.
MIT.
