Point clouds inside the map, not floating over it.
@mapmap/points draws lidar and photogrammetry clouds in the map’s own WebGL context: one buffer, one draw call, compositing with the basemap, the terrain and every other layer you already render. Ten bytes a point on the wire, your class bytes kept as written, and the same snk_ key that runs the rest of the platform.
50,000 free calls a month, card-free.
Point cloud: Stadt Wien - data.wien.gv.at, CC BY 4.0.
One draw call, ten bytes a point, your class table.
The package installs from npm today and renders payloads you host yourself, against the hosted gateway at api.mapmap.ai or your own self-hosted deployment.
Inside the map's WebGL context
A MapLibre CustomLayerInterface sharing the map's WebGL context. The cloud is depth-correct against terrain and 3D buildings, moves with the camera exactly as the basemap does, and costs one draw call. With terrain on, terrainRelative rebuilds the eye position from the terrain surface, so point sizes and fog distances are computed from where the camera actually is.
Ten bytes a point
Three Uint16 positions and four Uint8 bytes of colour, class in the alpha byte. About 48 bytes a point becomes 10, and both blocks reach the GPU untouched: decoding is two typed-array views over the downloaded ArrayBuffer, with no copy and no per-point loop. The shader dequantises.
Your classes, your colours
The SDK has no opinion about what a class byte means. Pass your table and your byte values survive: 40 stays 40. Colour by the payload's own RGB, flat per class, or a height ramp, and build the legend from getClasses().
Bake it offline, then add one layer at runtime.
Bake to the 10-byte format, offline.
Production baking happens offline, in Python or Rust, and produces one JSON sidecar and one binary blob. encodePointCloud exists for small clouds built in the browser. The sidecar carries anchor as [lng, lat], quant in metres per position unit, origin, count, the local bounding box, the class table and terrainRelative. validateMeta fails a sidecar whose extent will not fit the format rather than letting a cloud fold back on itself: at the default 2 cm quantisation a Uint16 axis spans 1.3 km.
One draw call.
Positions and colours are uploaded as two interleaved buffers and drawn as a single THREE.Points call inside the map’s context. There is no level-of-detail tree and no paging: everything is resident, which keeps the layer simple and puts a hard ceiling on how big a cloud can be.
The class table is an input.
| Field | Meaning |
|---|---|
| value | Your class byte, 0 to 255, preserved as written |
| label | What the legend shows |
| colour | #rrggbb or an [r, g, b] triple in 0 to 1, optional |
| visible | Initial visibility, optional |
Up to 16 classes per layer, a WebGL1 uniform-budget limit. Duplicate bytes, out-of-range values and oversized tables throw at load. Omit the table entirely and you get RGB and height colouring, and setColourMode("class") throws rather than inventing a taxonomy.

Colour modes and runtime controls.
| Mode | What it shows |
|---|---|
| rgb | The payload's own colours, the default |
| class | Flat per-class colours from your table |
| height | A ramp over local height |
Drive it from your own UI: setColourMode, setClassVisible, setAllClassesVisible, setPointSize in metres, getFps for a rolling one-second average, getClasses for the resolved table. A point is 0.15 m in the world by default and capped at 10 device pixels on screen, which you raise for offline video bakes.

Contains Environment Agency data. © Environment Agency and database right. Open Government Licence v3.0.
Install it, then add the layer.
npm install @mapmap/points three maplibre-gl<div id="map" style="height: 480px"></div>import { classesFromMeta, createPointCloudLayer } from "@mapmap/points";
const [meta, raw] = await Promise.all([
fetch("/cloud.json").then((r) => r.json()),
fetch("/cloud.bin").then((r) => r.arrayBuffer()),
]);
map.on("style.load", () =>
map.addLayer(
createPointCloudLayer({ meta, raw }, { classes: classesFromMeta(meta.classes ?? {}) }),
),
);three and maplibre-gl are peer dependencies, and the app must provide a single shared copy of each: two MapLibre instances on one page break the WebGL context, and two copies of three give you two incompatible sets of classes. @mapmap/points is v0.3.0, ESM only, and needs Node 18 or later to build. See the full SDK reference.
A renderer, not a platform.
You should know where it stops before you spend a day on a trial.
One COPC per layer. openCopc streams a single file over HTTP range requests. Multi-file mosaics and EPT sources are not in 0.3.0, and a plain LAZ is not a COPC: pdal translate converts one in a single command.
The baked path is still not an LOD system. A 10-byte payload is fetched whole and stays resident, which works comfortably to a few million points on a modern GPU. Past that, bake to COPC and stream it, or split the area into several clouds and swap layers by viewport.
CRS support has edges. A streamed COPC is placed from its own WKT record: geographic, Web Mercator, Transverse Mercator (every UTM zone, BNG, MTM) and Lambert Conformal Conic 2SP. Anything else throws UnsupportedCrsError naming the CRS, and a toWgs84 hook takes you past it. The baked path reprojects nothing at all: the sidecar's anchor is trusted as lng/lat, so check an uncertain dataset first with validate_geodata or POST /geodata/validate. Neither path performs a datum shift.
No measurement, annotation or share links. None of the three exist in the SDK today.
The points are not a metered event.
You host the payloads and the package renders them, so drawing the points never hits our meter. What meters is the map underneath: the same snk_ key as the rest of the platform, issued card-free with 50,000 free calls a month, or nothing at all once the stack is running on your own infrastructure.
If the cloud is not baked yet, that is a fixed-price package rather than a subscription. A site or corridor bake is £3,600: we take your survey delivery, bake it to the wire format, agree the class table as you number it, check the declared CRS against the coordinates and stand a viewer up on the deployment you will actually use, then hand over the payload and its sidecar for you to serve. A complex bake, where the classification is mixed or undocumented or the CRS has to be reconciled before anything can be trusted, is £6,000. Larger programmes are scoped per project. After handover there is nothing recurring: no per-point charge, no per-payload charge, no charge for looking.
If you would rather we hosted and served the payloads too, that is live, self-serve and priced. Storage is £1 per GB a month with a £10 monthly minimum, billed monthly against measured stored bytes, pro-rata to the byte. Streaming meters as standard calls on the same snk_ key as everything else: one standard call per five payload requests, counted in started 4 MiB pieces, so a whole-file download meters exactly as fetching it in chunks would. Payloads are baked and staged by us rather than uploaded self-serve, and rendering payloads you host yourself stays free and unmetered: hosting on MapMap is optional, never required.
Issue a key and put a cloud on the map.
Card-free, with 50,000 free calls a month. Bake a corridor, fetch two files, add a layer.
