Cesiumjs interaction
Curated agent skills for CesiumJS development.
npx -y skills add CesiumGS/cesiumjs-skills --skill cesiumjs-interactionAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
What its author says it does
Copied from the file, not written here
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.
SKILL.md
14.7 KB, as published. Nobody here has run it
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
pickAsyncoverpickon MOUSE_MOVE -- synchronous pick stalls the GPU pipeline;pickAsyncyields to the GPU and resolves next frame (WebGL2, v1.136+). - Use
drillPickwith alimit-- without one, it re-renders the scene for every overlapping object. - Avoid
pickin MOUSE_MOVE when only click picking is needed -- MOUSE_MOVE fires on every pointer move and triggers a pick render pass each time. - Enable
depthTestAgainstTerrainfor accuratepickPositionresults 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.pickPositionSupportedbefore usingpickPosition-- falls back tocamera.pickEllipsoidon unsupported GPUs. - Set
scene.pickTranslucentDepth = trueonly when needed -- adds an extra render pass. - Reuse result objects -- pass a scratch
Cartesian3topickPositionto avoid GC pressure in MOUSE_MOVE. - Use
scene.requestRenderMode = truewith picking to avoid unnecessary renders; callscene.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