From cesiumjs-skills
Handles CesiumJS user interactions via ScreenSpaceEventHandler for mouse/touch events and Scene picking (pick, drillPick, pickPosition). Use for globe clicks, entity/3D Tiles selection, hovers, drags.
npx claudepluginhub cesiumgs/cesiumjs-skills --plugin cesiumjs-skillsThis skill uses the workspace's default tool permissions.
Version baseline: CesiumJS v1.139 (ES module imports, Ion token required).
Loads CesiumJS 3D Tilesets via async factories from URLs, Cesium ion, Google Photorealistic 3D, or OSM Buildings. Covers styling, metadata querying, feature picking, voxels, point clouds, I3S, Gaussian splats, and clipping planes/polygons.
Implements Three.js interactions: raycasting for object picking, mouse/touch input handling, OrbitControls for camera. Use for user input in 3D scenes.
Implements Three.js interactions: raycasting for object selection and click detection, OrbitControls for camera navigation, mouse/touch input handling. Use for interactive 3D web scenes.
Share bugs, ideas, or general feedback.
Version baseline: CesiumJS v1.139 (ES module imports, Ion token required).
Central class for mouse, touch, and pointer events on the Cesium canvas.
import { ScreenSpaceEventHandler, ScreenSpaceEventType,
KeyboardEventModifier, defined } from "cesium";
const 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);
// Query or remove actions
const action = handler.getInputAction(ScreenSpaceEventType.LEFT_CLICK);
handler.removeInputAction(ScreenSpaceEventType.LEFT_CLICK);
// 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.
| 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.
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.
// Pick a voxel cell and read its properties
const voxelCell = viewer.scene.pickVoxel(event.position);
if (defined(voxelCell)) {
console.log(voxelCell.getProperty("temperature"));
}
| 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() |
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);
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);
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).
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
pickAsync over pick on MOUSE_MOVE -- synchronous pick stalls the GPU pipeline; pickAsync yields to the GPU and resolves next frame (WebGL2, v1.136+).drillPick with a limit -- without one, it re-renders the scene for every overlapping object.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.depthTestAgainstTerrain for accurate pickPosition results over terrain.scene.pickPositionSupported before using pickPosition -- falls back to camera.pickEllipsoid on unsupported GPUs.scene.pickTranslucentDepth = true only when needed -- adds an extra render pass.Cartesian3 to pickPosition to avoid GC pressure in MOUSE_MOVE.scene.requestRenderMode = true with picking to avoid unnecessary renders; call scene.requestRender() only on state changes.