npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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

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-build

examples/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 node

Converting

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.