v3.0.0

<bmx-point-cloud>

Shows a point cloud - a laser scan or a photogrammetry survey - in the page: LAS and LAZ (the ASPRS formats, LAZ decompressed in the browser), PLY, PCD and plain XYZ text. Millions of points draw smoothly because the cloud is cut into an octree of detail levels and each frame draws the near part in full and the far part as a sample, within a point budget. Colour the points by the file's own colour, intensity, elevation, classification or return number; hide classes; turn, move and zoom; pick standard views and an orthographic projection; read the file's facts; and save a picture.

21 properties · 7 events · 15 methods · 5 parts

Example

Try it with your own scans in confidence: LAS, LAZ, PLY, PCD and XYZ files are opened by your browser, on your device. Nothing is uploaded, no copy reaches us or anyone else, and nothing is kept once you close this tab.

Making a survey…

Show markup
<bmx-point-cloud id="ex-pc" label="Quarry survey" allow-open color-mode="auto" style="--bmx-point-cloud-height: 30rem"
  annotations='[{"id":"n1","text":"Site shed - roof scanned from the drone","position":[300105,5000127.5,44]},{"id":"n2","text":"Stockpile, measured 14 October","position":[300060,5000060,52]}]'></bmx-point-cloud>
<p class="note" id="ex-pc-out" role="status" style="margin: 12px 0 0">Making a survey&hellip;</p>

<script type="module">
  await customElements.whenDefined('bmx-point-cloud');
  const viewer = document.getElementById('ex-pc');
  const out = document.getElementById('ex-pc-out');

  // A made-up survey written as a binary PLY in memory: a quarry floor with benches cut into one side,
  // a stockpile, a shed, a few trees and some stray points high up - the shapes a laser scan of a site
  // has, so that colouring by elevation, intensity and classification each shows something. The
  // points are invented here, not read from any real survey.
  let seed = 20261021;
  const rnd = () => ((seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296);
  const N = 180000;
  const pts = [];
  for (let i = 0; i < N; i += 1) {
    const x = rnd() * 240;
    const y = rnd() * 180;
    // The floor dips towards the middle; benches step up on the east side.
    let z = 40 - 6 * Math.exp(-(((x - 120) / 70) ** 2 + ((y - 90) / 50) ** 2)) + 0.8 * Math.sin(x / 9) * Math.cos(y / 11);
    if (x > 170) z += 5 * Math.floor((x - 170) / 18);
    let cls = 2; // ground
    const stockpile = Math.hypot(x - 60, y - 60);
    if (stockpile < 22) { z += (22 - stockpile) * 0.55; cls = 2; }
    if (x > 95 && x < 115 && y > 120 && y < 135) { z = 44; cls = 6; } // the shed roof
    const tree = [[30, 150], [45, 160], [200, 30], [215, 45], [150, 170]].find(([tx, ty]) => Math.hypot(x - tx, y - ty) < 5);
    if (tree) { z += 4 + rnd() * 7; cls = 5; }
    if (rnd() < 0.0015) { z += 15 + rnd() * 20; cls = 7; } // noise: birds, dust
    const intensity = cls === 6 ? 2600 + rnd() * 400 : cls === 5 ? 600 + rnd() * 500 : 1200 + 900 * (0.5 + 0.5 * Math.sin(x / 15)) + rnd() * 200;
    pts.push(x + rnd() * 0.08, y + rnd() * 0.08, z + rnd() * 0.05, Math.round(intensity), cls);
  }
  const header = `ply\nformat binary_little_endian 1.0\ncomment Invented survey for the BMX docs\nelement vertex ${N}\nproperty double x\nproperty double y\nproperty float z\nproperty ushort intensity\nproperty uchar classification\nend_header\n`;
  const head = new TextEncoder().encode(header);
  const body = new Uint8Array(N * 23);
  const view = new DataView(body.buffer);
  for (let i = 0; i < N; i += 1) {
    const at = i * 23;
    view.setFloat64(at, 300000 + pts[i * 5], true);
    view.setFloat64(at + 8, 5000000 + pts[i * 5 + 1], true);
    view.setFloat32(at + 16, pts[i * 5 + 2], true);
    view.setUint16(at + 20, pts[i * 5 + 3], true);
    body[at + 22] = pts[i * 5 + 4];
  }
  const file = new Uint8Array(head.length + body.length);
  file.set(head, 0);
  file.set(body, head.length);

  viewer.addEventListener('bmxLoad', e => {
    const d = e.detail;
    out.textContent = `${d.points.toLocaleString()} points read in ${(d.millis / 1000).toFixed(1)} s. Colour by classification and untick Ground to see what stands on it; drag to turn, right-drag to move, wheel to zoom, click a point for its coordinates. Measure the shed, cut a profile across the benches, box in the stockpile, and save the view.`;
  });
  viewer.addEventListener('bmxPick', e => {
    const p = e.detail;
    out.textContent = `Point ${p.index.toLocaleString()} at ${p.position.map(v => v.toFixed(2)).join(', ')}` + (p.classification !== null ? `, class ${p.classification}` : '') + (p.intensity !== null ? `, intensity ${p.intensity}` : '');
  });
  viewer.addEventListener('bmxMeasure', e => {
    const m = e.detail;
    out.textContent = m.kind === 'area' ? `Area ${m.area.toFixed(2)} m², perimeter ${m.length.toFixed(2)} m` : `${m.kind === 'height' ? 'Height' : 'Distance'} ${m.length.toFixed(3)} m`;
  });
  viewer.addEventListener('bmxProfile', e => {
    out.textContent = `Profile of ${e.detail.samples.toLocaleString()} points over ${Math.hypot(e.detail.to[0] - e.detail.from[0], e.detail.to[1] - e.detail.from[1]).toFixed(1)} m; the chart is in the Profile tab, and the CSV button saves it.`;
  });
  viewer.addEventListener('bmxError', e => {
    out.textContent = e.detail.message;
  });
  await viewer.load(file, { name: 'quarry.ply' });
</script>
<bmx-point-cloud src="/surveys/site.laz" label="Site survey" color-mode="elevation"></bmx-point-cloud>

Everything runs in the browser: the file is decoded on background threads and drawn with WebGL 2; nothing is uploaded. A file given by address is read in ranges, so a large LAZ shows while its chunks are still arriving.

Keyboard: the view takes focus with Tab. Arrow keys turn the cloud, Shift with the arrows moves it, plus and minus zoom, Home fits it, 1 to 6 choose top, front, back, left, right and isometric, P switches projection, C cycles the colouring, Enter reports the point under the cross in the middle.

Properties

PropertyAttributeTypeDefaultDescription
allowOpen allow-open boolean false Offer an Open button, and open files dropped on the view.
annotations annotations BmxPointAnnotation[] | string [] Notes pinned to points, as an array or JSON. Two-way: bmxChange reports edits.
clipBox clip-box BmxPointClipBox | string | null null Only points inside this box are drawn ({min: [x, y, z], max: [x, y, z]} in the file's coordinates, or JSON); null for all. Two-way.
colorMode color-mode BmxPointColorMode | 'auto' 'auto' How the points are coloured. auto uses the file's colour when it has one, else elevation. Two-way.
hiddenClasses hidden-classes number[] | string [] Classification codes not drawn, as an array or JSON. Two-way: bmxChange reports edits.
label label string — The cloud's name for assistive technology and the toolbar. Default: its file name.
panel panel boolean true Show the side panel (information, classes). Two-way (the toolbar's panel button).
pointBudget point-budget number 2_000_000 The most points drawn in one frame. Two-way.
pointColor point-color string '' The colour for the single-colour mode (any CSS colour).
pointSize point-size number 1 Point size: 1 is the natural size, 0.5 half, 2 double. Two-way.
profileWidth profile-width number 1 How wide the band of points a profile gathers is, in the file's units. Two-way.
projection projection 'perspective' | 'orthographic' 'perspective' Two-way.
readonly readonly boolean false Notes, measurements, views and the box can be read but not added, changed or removed.
shading shading 'edl' | 'none' 'edl' Shade points by their depth against their neighbours (eye-dome lighting), so surfaces read without lights.
shadingStrength shading-strength number 1 How strong the shading is, 0 to 2.
sizing sizing 'adaptive' | 'fixed' 'adaptive' adaptive sizes each point by the detail around it (no holes close up); fixed draws every point the same size.
snapshots snapshots BmxPointSnapshot[] | string [] Saved views, as an array or JSON. Two-way: bmxChange reports edits.
src src string — The file's address. LAS and LAZ are read in ranges when the server allows it.
strings strings Partial<BmxPointCloudStrings> | string — Words to show instead of the English ones, as an object or JSON.
tool tool BmxPointCloudTool 'none' The tool in use: pick points on the cloud to measure, cut a profile, add a note or set the clipping box. Two-way.
toolbar toolbar boolean true Show the toolbar.

Events

EventDetailDescription
bmxChange BmxPointCloudChangeDetail The hidden classes, notes, saved views or clipping box changed.
bmxError { readonly message: string; } A cloud could not be read.
bmxLoad BmxPointCloudLoadDetail A cloud was read and shown.
bmxMeasure BmxPointMeasurement & { readonly length: number; readonly area: number; } A measurement was taken.
bmxPick BmxPointCloudPickDetail A point was picked (a click, or Enter on the view).
bmxProfile { readonly from: BmxPointXyz; readonly to: BmxPointXyz; readonly width: number; readonly samples: number; } A profile was cut.
bmxProgress BmxPointCloudProgressDetail Reading is under way: how far, and what is happening.

Methods

MethodSignatureDescription
applySnapshot applySnapshot(id: string) => Promise<void> Returns to a saved view.
clearMeasurements clearMeasurements() => Promise<void>
cutProfile cutProfile(from: BmxPointXyz, to: BmxPointXyz, width?: number) => Promise<BmxPointProfile | null> Cuts a profile along a line (two points in the file's coordinates) and shows it.
fit fit() => Promise<void> Fits the camera to the whole cloud.
getCamera getCamera() => Promise<BmxPointCloudCamera>
getClasses getClasses() => Promise<{ code: number; name: string; count: number; visible: boolean; }[]> The classification codes the file uses, with their names and counts.
getInfo getInfo() => Promise<BmxPointCloudLoadDetail | null> What the file says about itself.
getMeasurements getMeasurements() => Promise<(BmxPointMeasurement & { length: number; area: number; })[]> The measurements taken, with their results.
getProfile getProfile() => Promise<BmxPointProfile | null> The last profile cut, or null.
load load(data: ArrayBuffer | Uint8Array | string | Blob, options?: BmxPointCloudLoadOptions) => Promise<void> Shows a cloud from its data: bytes, text, or a File or Blob.
screenshot screenshot(options?: { type?: "image/png" | "image/jpeg" | "image/webp"; quality?: number; background?: boolean; }) => Promise<Blob | null> The picture as an image. With background, the view's background is laid under it (always, for JPEG).
setCamera setCamera(camera: Partial<BmxPointCloudCamera>) => Promise<void> Moves the camera (a saved view, for example). Parts left out keep their values.
setClassVisible setClassVisible(code: number, visible: boolean) => Promise<void> Shows or hides a classification.
setView setView(view: BmxPointCloudView) => Promise<void> Looks from a standard direction, keeping what is looked at.
takeSnapshot takeSnapshot(name?: string) => Promise<BmxPointSnapshot> Saves the camera and the way the cloud is shown as a view.

CSS shadow parts

PartDescription
panel The side panel (information, classes).
pin
stage The area the cloud is drawn in.
status The line under the view.
toolbar The bar of commands.

CSS custom properties

PropertyDescription
--bmx-point-cloud-background Behind the points.
--bmx-point-cloud-box The clipping box's outline.
--bmx-point-cloud-height The height of the view.
--bmx-point-cloud-measure Measurement lines and points.
--bmx-point-cloud-panel-width The side panel's width.
--bmx-point-cloud-pin A note's pin.
--bmx-point-cloud-point The colour of points in the single-colour mode (when point-color is not set).
--bmx-point-cloud-profile The profile line, and the profile chart's marks without classes.
--bmx-point-cloud-radius Corner radius of the frame.
--bmx-point-cloud-surface The colour a saved picture is laid on.