---
title: "How do I let the user pick a position in the scene?"
section: "Web Navigator"
route: /faqscenepickerinplugins
account: {accountId}
bruce_api: https://{accountId}.api.nextspace.host
guardian_api: https://guardian.nextspace.host
---
# 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`.

```javascript
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**

```javascript
/**
 * 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();
    };
}
```

**Clamping to the ground**

```javascript
async function groundedPosition(params, pos3d) {
    const { viewer, Cesium, BEngine } = params.pluginParams;

    const carto = Cesium.Cartographic.fromCartesian(pos3d);
    const terrain = await BEngine.DrawingUtils.GetTerrainHeight({
        pos3d: pos3d,
        viewer: viewer
    });

    // The sample can fail, in which case keep the picked height
    // rather than dropping the marker to the ellipsoid.
    if (terrain.error) {
        return pos3d;
    }

    return Cesium.Cartesian3.fromRadians(
        carto.longitude,
        carto.latitude,
        terrain.height
    );
}
```

**Picking an Entity instead**

```javascript
function Run(params) {
    const { viewer, Cesium, visualRegister, select } = params.pluginParams;

    const handler = new Cesium.ScreenSpaceEventHandler(viewer.canvas);
    handler.setInputAction((e) => {
        const { regos } = visualRegister.GetRegosFromCursor({
            cursor: e.position
        });
        if (!regos.length) {
            return;
        }

        const rego = regos[0];
        console.log(rego.entityId, rego.entityTypeId, rego.name);

        // Drive Navigator's selection rather than tracking your own.
        select({
            entityIds: [rego.entityId]
        });
    }, Cesium.ScreenSpaceEventType.LEFT_CLICK);

    // The current selection at any time, and a subscription to it.
    console.log(visualRegister.GetSelected());
    const unsubscribe = visualRegister.OnUpdate.Subscribe((data) => {
        console.log("Selection changed", data);
    });

    return () => {
        handler.destroy();
        unsubscribe?.();
    };
}
```

---

Urls on this page are resolved for account `{accountId}`.
Site index: https://docs.nextspace.host/llms.txt · whole site in one file: https://docs.nextspace.host/llms-full.txt
Human-readable version of this page: https://docs.nextspace.host/faqscenepickerinplugins
