How do I let the user pick a position in the scene?


This is the most common shape a Plugin takes: the user clicks something in the 3D scene, a marker appears where they clicked, and the Plugin does something with that position. Placing an asset, dropping a note, starting a measurement and reading a coordinate are all the same three steps.

A Cursor bar Plugin is the right location for it. Enabling the tool starts the pick, and the Plugin's dispose function ends it, so the user turning the tool off cleans up without the Plugin tracking any state of its own.

Resolving a screen position to a world position

Cesium offers several ways to turn a mouse position into a world position and they do not agree: picking the globe ignores tilesets, picking the ellipsoid ignores terrain, and picking a primitive misses ground. bruce-cesium resolves all of that in one place, as DrawingUtils.GetAccuratePosition.

const { BEngine } = params.pluginParams;

// pos2d is a Cesium.Cartesian2 in canvas pixels.
// Returns a Cesium.Cartesian3 in absolute coords or null when nothing was found.
const pos3d = BEngine.DrawingUtils.GetAccuratePosition(viewer, pos2d);

// The third argument restricts the resolve to picked geometry only,
// so a click that misses a model or tileset returns nothing rather.
const onGeometryOnly = BEngine.DrawingUtils.GetAccuratePosition(viewer, pos2d, true);

A complete picker

The pattern below tracks the cursor while the tool is enabled, places a marker on click, and disposes everything it created.


index.js
Clamping to the ground
Picking an Entity instead
/**
 * Cursor bar Plugin.
 * Click in the scene to place a marker and read the position back.
 */
function Run(params) {
    const {
        viewer, Cesium, BEngine, close
    } = params.pluginParams;
    const { Cartes } = params.pluginParams.BModels;
    const { GetAccuratePosition } = BEngine.DrawingUtils;

    // Everything the Plugin adds to the scene, so dispose can be
    // one loop rather than a list of special cases.
    const created = [];

    // Last valid world position under the cursor.
    let hovered = null;

    /**
     * Adds a point at a world position and returns its Cesium entity.
     */
    function placeMarker(pos3d, label) {
        const marker = viewer.entities.add({
            position: pos3d,
            point: {
                pixelSize: 12,
                color: Cesium.Color.fromCssColorString("#33B1FF"),
                outlineColor: Cesium.Color.WHITE,
                outlineWidth: 2,
                // Keeps the marker visible through geometry in front of it.
                disableDepthTestDistance: Number.POSITIVE_INFINITY
            },
            label: {
                text: label,
                font: "14px sans-serif",
                fillColor: Cesium.Color.WHITE,
                showBackground: true,
                pixelOffset: new Cesium.Cartesian2(0, -24),
                disableDepthTestDistance: Number.POSITIVE_INFINITY
            }
        });
        created.push(marker);

        viewer.scene.requestRender();
        return marker;
    }

    const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);

    // Track the cursor.
    handler.setInputAction((e) => {
        if (!Cartes.ValidateCartes2(e.endPosition)) {
            return;
        }
        const pos3d = GetAccuratePosition(viewer, e.endPosition);
        if (Cartes.ValidateCartes3(pos3d)) {
            hovered = pos3d;
        }
    }, Cesium.ScreenSpaceEventType.MOUSE_MOVE);

    // Place on click.
    handler.setInputAction(async (e) => {
        if (!Cartes.ValidateCartes2(e.position)) {
            return;
        }
        const pos3d = GetAccuratePosition(viewer, e.position);
        if (!Cartes.ValidateCartes3(pos3d)) {
            return;
        }

        // Degrees and metres, which is what an API call wants.
        const carto = Cesium.Cartographic.fromCartesian(pos3d);
        const lon = Cesium.Math.toDegrees(carto.longitude);
        const lat = Cesium.Math.toDegrees(carto.latitude);

        placeMarker(pos3d, lat.toFixed(6) + ", " + lon.toFixed(6));

        await onPicked({ lat, lon, alt: carto.height, pos3d });
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    // Right click ends the tool. close() disables the cursor.
    handler.setInputAction(() => {
        close();
    }, Cesium.ScreenSpaceEventType.RIGHT_CLICK);

    /**
     * Whatever the Plugin exists to do with a picked position.
     */
    async function onPicked(picked) {
        console.log("Picked", picked);
    }

    return () => {
        handler.destroy();
        created.forEach((entity) => viewer.entities.remove(entity));
        created.length = 0;
        viewer.scene.requestRender();
    };
}