Skip to content

Tooltips and selection

Picking on volumes, clouds, splats ​

scenes/picking-on-volumes-clouds-splats
const { positions, scalars } = clusters(3000);
plot.showPoints(plot.uploadPoints({ positions, scalars }));
plot.frameScene();
return {
  async pointer(x, y) {
    const hit = await plot.pickScene(x, y, true);
    if (hit === null) return "nothing within six pixels";
    const at = [0, 1, 2].map((c) => fixed(positions[hit.index * 3 + c])).join(", ");
    return `point ${hit.index} at (${at}), value ${fixed(scalars[hit.index])}`;
  },
};

Calls

4 in the code above, in the order it makes them

plot.showPoints(handle: PointsHandle): void

Draw the point cloud handle names.

plot.uploadPoints(points: PointSet): PointsHandle

Upload a point cloud and return a handle naming it.

plot.frameScene(): void

Point the camera at the middle of everything uploaded, far enough back to see it.

Call after uploading. A scene measured in grid cells or in millimetres is nowhere near where a camera starts, and without this the first thing a caller sees is an empty canvas.

plot.pickScene(x: number, y: number, detail?: boolean, radius?: number): Promise<ScenePick | null>

What scene item is under a pixel: a volume, a point cloud, a splat set, a vector field or a surface. null when the ray meets nothing.

The scientific geometry's answer to hitTest, which resolves a chart's datum. With detail, the pick also names the sub-object it struck -- the point, the splat, the arrow -- as an index into the array that was uploaded.

Resolves a frame or two later, because the answer comes from reading a pixel back off the GPU and a browser cannot wait on that. The page has to keep rendering for it to land, which a plot being looked at does anyway.

radius is how far, in logical pixels, the pointer may be from what it picks. A point in a cloud is a disc a few pixels across and a user aims at it rather than hitting it, so the default is a tolerance, not a pixel: within it the most specific feature wins (a point, a splat or an arrow before a surface), ties broken by distance. Pass 0 for the pixel under the pointer and nothing else. A near pick carries no value: the scalar at a point the pointer was merely near is not read back.

A pick started while another is in flight replaces it, so the earlier promise resolves null rather than hanging: a pointer crossing the canvas costs one pick, not a queue of them.

On WebGL2 there is no pixel to read back. The pick is a ray cast on the CPU against the next frame, which turns setScenePicking on; radius is ignored there, and isosurfaces are not reached.