mcpbeat

Cesiumjs Interaction

cesiumgs/cesiumjs-interaction

CesiumJS interaction and picking - ScreenSpaceEventHandler, multi-key KeyboardEventModifier input actions, Scene.pick, Scene.drillPick, Scene.pickPosition, mouse and touch events. Use when handling user clicks on the globe, selecting entities or 3D Tiles features, registering modifier-key shortcuts, implementing hover effects, or building drag-based interactions.

4k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
110
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/CesiumGS/cesiumjs-skills --skill cesiumjs-interaction

The instruction itself

23 sections, as written by the author

CesiumJS Interaction & Picking

Version baseline: CesiumJS v1.143 (ES module imports, Ion token required).

ScreenSpaceEventHandler

Central class for mouse, touch, and pointer events on the Cesium canvas.

import { ScreenSpaceEventHandler, ScreenSpaceEventType,
  KeyboardEventModifier, defined } from "cesium";

let handler = new ScreenSpaceEventHandler(viewer.scene.canvas);

// Register a click handler
handler.setInputAction((event) => {
  console.log("Clicked at", event.position.x, event.position.y);
}, ScreenSpaceEventType.LEFT_CLICK);

// With keyboard modifier (Shift+Click)
handler.setInputAction((event) => {
  console.log("Shift+Click at", event.position);
}, ScreenSpaceEventType.LEFT_CLICK, KeyboardEventModifier.SHIFT);

// With multiple modifiers (Ctrl+Shift+Click, 1.142+)
handler.setInputAction((event) => {
  console.log("Ctrl+Shift+Click at", event.position);
}, ScreenSpaceEventType.LEFT_CLICK, [
  KeyboardEventModifier.CTRL,
  KeyboardEventModifier.SHIFT,
]);

// Query or remove actions
const clickAction = handler.getInputAction(ScreenSpaceEventType.LEFT_CLICK);
const ctrlShiftClickAction = handler.getInputAction(ScreenSpaceEventType.LEFT_CLICK, [
  KeyboardEventModifier.SHIFT,
  KeyboardEventModifier.CTRL, // order does not matter
]);
if (defined(clickAction) && defined(ctrlShiftClickAction)) {
  console.log("Click handlers registered");
}
handler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
handler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK, [
  KeyboardEventModifier.CTRL,
  KeyboardEventModifier.SHIFT,
]);

// Always destroy when done to avoid memory leaks
handler = handler && handler.destroy();

The Viewer also has a built-in handler at viewer.screenSpaceEventHandler -- use it to avoid creating a second handler for simple cases.

ScreenSpaceEventType Reference

| Event | Callback shape | Notes |

|---|---|---|

| LEFT_DOWN / LEFT_UP / LEFT_CLICK | ({ position }) | Cartesian2 screen coords |

| LEFT_DOUBLE_CLICK | ({ position }) | Left only |

| RIGHT_DOWN / RIGHT_UP / RIGHT_CLICK | ({ position }) | |

| MIDDLE_DOWN / MIDDLE_UP / MIDDLE_CLICK | ({ position }) | |

| MOUSE_MOVE | ({ startPosition, endPosition }) | Fires on every pointer move |

| WHEEL | (delta) | Positive = scroll up |

| PINCH_START | ({ position1, position2 }) | Two-finger touch begins |

| PINCH_END | () | Two-finger touch ends |

| PINCH_MOVE | ({ distance, angleAndHeight }) | Two-finger move |

KeyboardEventModifier: SHIFT, CTRL, ALT -- optional third argument to

setInputAction, getInputAction, and removeInputAction. In 1.142+, pass a

single modifier or an array of modifiers. Modifier arrays are order-independent

but exact: a handler registered for [CTRL, SHIFT] does not fire when ALT is

also held.

Scene Picking Methods

pick / pickAsync / drillPick / pickPosition

import { Cartographic, Math as CesiumMath, defined } from "cesium";

// pick -- synchronous, returns top-most object or undefined
const picked = viewer.scene.pick(event.position);

// pickAsync -- non-blocking (WebGL2, v1.136+), falls back to sync on WebGL1
const picked2 = await viewer.scene.pickAsync(movement.endPosition);

// drillPick -- all objects at position, front-to-back; use limit to cap cost
const allPicked = viewer.scene.drillPick(event.position, 5);

// pickPosition -- world Cartesian3 from depth buffer
if (viewer.scene.pickPositionSupported) {
  const cartesian = viewer.scene.pickPosition(event.position);
  if (defined(cartesian)) {
    const c = Cartographic.fromCartesian(cartesian);
    console.log(CesiumMath.toDegrees(c.longitude), CesiumMath.toDegrees(c.latitude), c.height);
  }
}

Set scene.pickTranslucentDepth = true to include translucent primitives in pickPosition.

pickVoxel (experimental)

// Pick a voxel cell and read its properties
const voxelCell = viewer.scene.pickVoxel(event.position);
if (defined(voxelCell)) {
  console.log(voxelCell.getProperty("temperature"));
}

Picking Return Values

| Picked object | Return shape | Key properties |

|---|---|---|

| Entity | { primitive, id } | id is the Entity instance |

| Cesium3DTileFeature | Cesium3DTileFeature | .getProperty(name), .getPropertyIds(), .color |

| Billboard/Label (collection) | { primitive, id } | id is the user-set id |

| Primitive (geometry) | { primitive, id } | id is the GeometryInstance id |

| Globe surface | undefined | Use camera.pickEllipsoid() or pickPosition() |

Recipes

1. Entity Selection with Click

handler.setInputAction((event) => {
  const picked = viewer.scene.pick(event.position);
  if (defined(picked) && defined(picked.id)) {
    viewer.selectedEntity = picked.id; // shows InfoBox
  } else {
    viewer.selectedEntity = undefined;
  }
}, ScreenSpaceEventType.LEFT_CLICK);

2. 3D Tiles Feature Picking and Property Inspection

import { Cesium3DTileFeature, Color } from "cesium";

handler.setInputAction((event) => {
  const picked = viewer.scene.pick(event.position);
  if (picked instanceof Cesium3DTileFeature) {
    // Read properties
    const ids = picked.getPropertyIds();
    ids.forEach((id) => console.log(`${id}: ${picked.getProperty(id)}`));
    picked.color = Color.YELLOW; // highlight
  }
}, ScreenSpaceEventType.LEFT_CLICK);

3. Terrain Position Picking (Lon/Lat from Click)

handler.setInputAction((event) => {
  const cartesian = viewer.camera.pickEllipsoid(
    event.position, viewer.scene.globe.ellipsoid);
  if (defined(cartesian)) {
    const c = Cartographic.fromCartesian(cartesian);
    console.log(`Lon: ${CesiumMath.toDegrees(c.longitude).toFixed(6)}`);
    console.log(`Lat: ${CesiumMath.toDegrees(c.latitude).toFixed(6)}`);
  }
}, ScreenSpaceEventType.LEFT_CLICK);

For height on 3D content, use scene.pickPosition instead (see above).

4. Multi-Pick with drillPick

import { EntityCollection, CallbackProperty, ColorMaterialProperty, Color } from "cesium";

const pickedEntities = new EntityCollection();
const highlightColor = Color.YELLOW.withAlpha(0.5);

// Make entity material react to selection state
function makePickable(entity, baseColor) {
  entity.polygon.material = new ColorMaterialProperty(
    new CallbackProperty((time, result) => {
      return pickedEntities.contains(entity)
        ? highlightColor.clone(result) : baseColor.clone(result);
    }, false));
}

handler.setInputAction((movement) => {
  const all = viewer.scene.drillPick(movement.endPosition);
  pickedEntities.removeAll();
  for (const p of all) {
    if (defined(p.id)) pickedEntities.add(p.id);
  }
}, ScreenSpaceEventType.MOUSE_MOVE);

5. Hover Highlighting with MOUSE_MOVE

import { Color } from "cesium";

const highlighted = { feature: undefined, originalColor: new Color() };

handler.setInputAction((movement) => {
  if (defined(highlighted.feature)) {
    highlighted.feature.color = highlighted.originalColor;
    highlighted.feature = undefined;
  }
  const picked = viewer.scene.pick(movement.endPosition);
  if (defined(picked) && defined(picked.color)) {
    highlighted.feature = picked;
    Color.clone(picked.color, highlighted.originalColor);
    picked.color = Color.YELLOW;
  }
}, ScreenSpaceEventType.MOUSE_MOVE);

6. Drag-Based Drawing and Measurement

import { Cartographic, EllipsoidGeodesic, Ellipsoid, Color } from "cesium";

const positions = [];

handler.setInputAction((event) => {
  const cartesian = viewer.camera.pickEllipsoid(
    event.position, viewer.scene.globe.ellipsoid);
  if (!defined(cartesian)) return;
  positions.push(cartesian);

  if (positions.length === 2) {
    viewer.entities.add({
      polyline: { positions: positions.slice(), width: 3,
        material: Color.RED, clampToGround: true },
    });
    const start = Cartographic.fromCartesian(positions[0]);
    const end = Cartographic.fromCartesian(positions[1]);
    const geodesic = new EllipsoidGeodesic(start, end, Ellipsoid.WGS84);
    console.log(`Distance: ${(geodesic.surfaceDistance / 1000).toFixed(2)} km`);
    positions.length = 0;
  }
}, ScreenSpaceEventType.LEFT_CLICK);

7. Coordinate Readout on Mouse Move

import { HorizontalOrigin, VerticalOrigin, Cartesian2 } from "cesium";

const coordLabel = viewer.entities.add({
  label: { show: false, showBackground: true, font: "14px monospace",
    horizontalOrigin: HorizontalOrigin.LEFT, verticalOrigin: VerticalOrigin.TOP,
    pixelOffset: new Cartesian2(15, 0) },
});

handler.setInputAction((movement) => {
  const cartesian = viewer.camera.pickEllipsoid(
    movement.endPosition, viewer.scene.globe.ellipsoid);
  if (defined(cartesian)) {
    const c = Cartographic.fromCartesian(cartesian);
    coordLabel.position = cartesian;
    coordLabel.label.show = true;
    coordLabel.label.text =
      `Lon: ${CesiumMath.toDegrees(c.longitude).toFixed(4)}\n` +
      `Lat: ${CesiumMath.toDegrees(c.latitude).toFixed(4)}`;
  } else {
    coordLabel.label.show = false;
  }
}, ScreenSpaceEventType.MOUSE_MOVE);

8. Conditional Behavior Based on Picked Object Type

import { Cesium3DTileFeature } from "cesium";

handler.setInputAction((event) => {
  const picked = viewer.scene.pick(event.position);
  if (!defined(picked)) {
    console.log("No object picked");
  } else if (picked instanceof Cesium3DTileFeature) {
    console.log("3D Tile feature:", picked.getProperty("name"));
  } else if (defined(picked.id) && defined(picked.id.position)) {
    viewer.selectedEntity = picked.id; // Entity
  } else if (defined(picked.primitive)) {
    console.log("Primitive:", picked.primitive.constructor.name);
  }
}, ScreenSpaceEventType.LEFT_CLICK);

9. pickAsync for Non-Blocking Hover (v1.136+)

const highlighted = { feature: undefined, originalColor: new Color() };

handler.setInputAction(async (movement) => {
  if (defined(highlighted.feature)) {
    highlighted.feature.color = highlighted.originalColor;
    highlighted.feature = undefined;
  }
  const picked = await viewer.scene.pickAsync(movement.endPosition);
  if (defined(picked) && defined(picked.color)) {
    highlighted.feature = picked;
    Color.clone(picked.color, highlighted.originalColor);
    picked.color = Color.YELLOW;
  }
}, ScreenSpaceEventType.MOUSE_MOVE);

10. Hover + Selection with Silhouettes (Full Pattern)

import { PostProcessStageLibrary, Color } from "cesium";

const scene = viewer.scene;
const silhouetteHover = PostProcessStageLibrary.createEdgeDetectionStage();
silhouetteHover.uniforms.color = Color.BLUE;
silhouetteHover.uniforms.length = 0.01;
silhouetteHover.selected = [];

const silhouetteSelect = PostProcessStageLibrary.createEdgeDetectionStage();
silhouetteSelect.uniforms.color = Color.LIME;
silhouetteSelect.uniforms.length = 0.01;
silhouetteSelect.selected = [];

scene.postProcessStages.add(
  PostProcessStageLibrary.createSilhouetteStage([silhouetteHover, silhouetteSelect]));

let selectedFeature;

viewer.screenSpaceEventHandler.setInputAction((movement) => {
  silhouetteHover.selected = [];
  const picked = scene.pick(movement.endPosition);
  if (defined(picked) && picked !== selectedFeature) {
    silhouetteHover.selected = [picked];
  }
}, ScreenSpaceEventType.MOUSE_MOVE);

viewer.screenSpaceEventHandler.setInputAction((event) => {
  silhouetteSelect.selected = [];
  const picked = scene.pick(event.position);
  if (defined(picked)) {
    selectedFeature = picked;
    silhouetteSelect.selected = [picked];
    silhouetteHover.selected = [];
  } else {
    selectedFeature = undefined;
  }
}, ScreenSpaceEventType.LEFT_CLICK);

11. Wheel Zoom with Custom Logic

handler.setInputAction((delta) => {
  // delta > 0 = scroll up (zoom in), delta < 0 = scroll out
  const zoomAmount = delta > 0 ? 0.9 : 1.1;
  viewer.camera.zoomIn(viewer.camera.positionCartographic.height * (1 - zoomAmount));
}, ScreenSpaceEventType.WHEEL);

12. Right-Click Context Menu

viewer.scene.canvas.addEventListener("contextmenu", (e) => e.preventDefault());

handler.setInputAction((event) => {
  const picked = viewer.scene.pick(event.position);
  if (defined(picked) && defined(picked.id)) {
    showContextMenu(event.position, picked.id); // your app logic
  }
}, ScreenSpaceEventType.RIGHT_CLICK);

13. Drag Interaction (Move an Entity)

let draggedEntity = null;
const sscc = viewer.scene.screenSpaceCameraController;

handler.setInputAction((event) => {
  const picked = viewer.scene.pick(event.position);
  if (defined(picked) && defined(picked.id)) {
    draggedEntity = picked.id;
    sscc.enableRotate = false;
    sscc.enableTranslate = false;
  }
}, ScreenSpaceEventType.LEFT_DOWN);

handler.setInputAction((movement) => {
  if (!defined(draggedEntity)) return;
  const cartesian = viewer.camera.pickEllipsoid(
    movement.endPosition, viewer.scene.globe.ellipsoid);
  if (defined(cartesian)) draggedEntity.position = cartesian;
}, ScreenSpaceEventType.MOUSE_MOVE);

handler.setInputAction(() => {
  draggedEntity = null;
  sscc.enableRotate = true;
  sscc.enableTranslate = true;
}, ScreenSpaceEventType.LEFT_UP);

Performance Tips

  • Prefer pickAsync over pick on MOUSE_MOVE -- synchronous pick stalls the GPU pipeline; pickAsync yields to the GPU and resolves next frame (WebGL2, v1.136+).
  • Use drillPick with a limit -- without one, it re-renders the scene for every overlapping object.
  • Avoid pick in MOUSE_MOVE when only click picking is needed -- MOUSE_MOVE fires on every pointer move and triggers a pick render pass each time.
  • Enable depthTestAgainstTerrain for accurate pickPosition results over terrain.
  • Destroy unused handlers -- each one registers DOM listeners that leak memory if not cleaned up.
  • Throttle expensive hover logic -- debounce to 50-100ms for operations beyond simple highlighting.
  • Check scene.pickPositionSupported before using pickPosition -- falls back to camera.pickEllipsoid on unsupported GPUs.
  • Set scene.pickTranslucentDepth = true only when needed -- adds an extra render pass.
  • Reuse result objects -- pass a scratch Cartesian3 to pickPosition to avoid GC pressure in MOUSE_MOVE.

10. Use scene.requestRenderMode = true with picking to avoid unnecessary renders; call scene.requestRender() only on state changes.

See Also

  • cesiumjs-entities -- Entity API, graphics types, DataSources
  • cesiumjs-3d-tiles -- Cesium3DTileset, Cesium3DTileFeature, styling, metadata
  • cesiumjs-camera -- Camera.pickEllipsoid, ScreenSpaceCameraController, flyTo

How to use it

Copy the folder

Take cesiumgs/cesiumjs-interaction from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.