关于 cesiumjs-interaction
面向 CesiumJS 开发者的交互实现参考技能,涵盖事件处理、场景拾取、坐标读数、悬停效果及地理场景的交互模式。
主要功能
- 01屏幕空间鼠标与触摸事件
- 02同步及异步拾取
- 03检查 3D Tiles 要素属性
- 04读数所需的坐标转换
- 05悬停高亮与选择
适用场景
- 01点击选择实体
- 02检查拾取到的 3D Tiles 要素
- 03指针移动时显示经纬度
- 04创建悬停或拖动交互
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ cesiumgs/ cesiumjs-skills --skill cesiumjs-interaction安装后在 Claude Code 中输入/cesiumjs-interaction确认已加载。
详细介绍
CesiumJS 交互与拾取
基准版本:CesiumJS v1.144(ES 模块导入,需要 Ion 令牌)。
ScreenSpaceEventHandler
用于处理 Cesium 画布上鼠标、触摸和指针事件的核心类。
交互逻辑始终应创建一个绑定到 viewer.scene.canvas
的新 ScreenSpaceEventHandler ——这样可以将监听器与 Cesium 默认的
相机和选择处理器隔离开来,也是以下所有示例中采用的惯用模式。
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();
Viewer 还通过 viewer.screenSpaceEventHandler 暴露了一个内置处理器,用于驱动默认行为(实体选择、双击跟踪)。你
可以将事件附加到该处理器,但对于任何较复杂的交互,建议创建自己的
new ScreenSpaceEventHandler(viewer.scene.canvas),这样你的处理器
可以独立释放,也不会与 Cesium 默认处理器冲突。
ScreenSpaceEventType 参考
| 事件 | 回调形式 | 说明 |
|---|---|---|
LEFT_DOWN / LEFT_UP / LEFT_CLICK | ({ position }) | Cartesian2 屏幕坐标 |
LEFT_DOUBLE_CLICK | ({ position }) | 仅限左键 |
RIGHT_DOWN / RIGHT_UP / RIGHT_CLICK | ({ position }) | |
MIDDLE_DOWN / MIDDLE_UP / MIDDLE_CLICK | ({ position }) | |
MOUSE_MOVE | ({ startPosition, endPosition }) | 每次指针移动时触发 |
WHEEL | (delta) | 正值 = 向上滚动 |
PINCH_START | ({ position1, position2 }) | 双指触摸开始 |
PINCH_END | () | 双指触摸结束 |
PINCH_MOVE | ({ distance, angleAndHeight }) | 双指移动 |
KeyboardEventModifier:SHIFT、CTRL、ALT ——可作为可选的第三个参数传给
setInputAction、getInputAction 和 removeInputAction。在 1.142 及更高版本中,可传入
单个修饰键或修饰键数组。数组中的修饰键顺序不限,但必须完全匹配:注册为
[CTRL, SHIFT] 的处理器在同时按住 ALT 时不会触发。
场景拾取方法
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);
}
}
将 scene.pickTranslucentDepth = true 设为 true,使 pickPosition 包含半透明图元。
pickVoxel(实验性)
// Pick a voxel cell and read its properties
const voxelCell = viewer.scene.pickVoxel(event.position);
if (defined(voxelCell)) {
console.log(voxelCell.getProperty("temperature"));
}
snap(实验性,1.144+)
scene.snap 会搜索窗口位置周围的屏幕空间区域,并
返回最佳吸附目标;它优先选择模型边缘(来自 CAD 风格的
EXT_mesh_primitive_edge_visibility 数据),而非表面;命中对象类型相同的情况下,选择距离光标最近的对象。可将其用于测量和
检查工具,让工具吸附到边缘,而不是原始光标命中位置。
import { defined } from "cesium";
// Search a 25x25 px region centered on the cursor
const result = viewer.scene.snap(movement.endPosition, { width: 25 });
if (defined(result)) {
// SceneSnapResult: { object, position, screenPosition, isEdge }
console.log(result.isEdge ? "edge" : "surface", result.position);
}
只有通过 Model 管线渲染的图元(3D Tiles 和 glTF
模型)支持吸附。吸附需要支持浮点颜色附件的 WebGL2
(EXT_color_buffer_float);如果不支持,或者区域内没有
可吸附的几何体,snap 会返回 undefined。
拾取返回值
| 拾取对象 | 返回形式 | 关键属性 |
|---|---|---|
| Entity | { primitive, id } | id 是 Entity 实例 |
| Cesium3DTileFeature | Cesium3DTileFeature | .getProperty(name)、.getPropertyIds()、.color |
| Billboard/Label(集合) | { primitive, id } | id 是用户设置的 id |
| Primitive(几何体) | { primitive, id } | id 是 GeometryInstance 的 id |
| Globe 表面 | undefined | 使用 camera.pickEllipsoid() 或 pickPosition() |
交互演示的相机取景
拾取和悬停演示必须渲染主体地理区域,而不是星空。近期出现的问题是相机朝向天空(俯仰角约为 -45° 或更浅),或者影像加载失败,导致场景一片漆黑。以下三条规则不可妥协:
- 在需要查看区域地图的场景中,使用
viewer.camera.setView(或设置duration: 0的flyTo),并明确将俯仰角设为CesiumMath.toRadians(-90)(天底方向)。不要依赖默认俯仰角。 - 将场景取景在足以容纳所有主体要素的高度。 城市级演示使用 50–200 km;州/区域级使用 1–3 Mm;多州或群岛级使用 1.5–4 Mm。如果图钉或多边形落在取景框外,即使程序检查通过,评审也会降低候选方案的评分。
- 区域图钉演示必须完整包含纬度范围,并留出余量。 查看西海岸的西雅图/旧金山/洛杉矶时,中心点需靠近 40.8 N、-121 W,高度约为 3.2–4.0 Mm,或者使用拟合后的矩形。若以 40 N 为中心、相机高度为 2.5 Mm,西雅图通常会落在取景框顶部边缘,导致显示不全。
import { Cartesian3, Math as CesiumMath } from "cesium";
viewer.camera.setView({
destination: Cartesian3.fromDegrees(-122.0, 37.5, 1_500_000),
orientation: { heading: 0.0, pitch: CesiumMath.toRadians(-90), roll: 0.0 },
});
坐标转换(读数必需)
向用户显示经度或纬度时(标签文本、控制台日志、HTML 覆盖层),必须使用 CesiumMath.toDegrees 将弧度转换为角度。Cartographic 角度始终以弧度表示;直接输出会产生毫无意义的数值。
import { Cartographic, Math as CesiumMath } from "cesium";
const c = Cartographic.fromCartesian(cartesian);
const lonDeg = CesiumMath.toDegrees(c.longitude);
const latDeg = CesiumMath.toDegrees(c.latitude);
// Never display c.longitude / c.latitude directly in a UI label.
示例
拾取示例的多边形设置
交互演示需要可拾取的多边形实体时,请使用
new PolygonHierarchy(Cartesian3.fromDegreesArray([...])) 构建层级结构。PolygonHierarchy
没有静态的 fromDegrees()、fromDegreesArray() 或
fromEquatorialCoordinates() 辅助方法——调用这些方法会在运行时抛出
Cesium.PolygonHierarchy.fromDegrees is not a function,并导致场景渲染为空。唯一正确的构造方式是:
import { PolygonHierarchy, Cartesian3 } from "cesium";
const hierarchy = new PolygonHierarchy(
Cartesian3.fromDegreesArray([
-87.6, 41.8,
-85.6, 41.8,
-85.6, 43.3,
-87.6, 43.3,
]),
);
viewer.entities.add({
polygon: { hierarchy, material: Color.CRIMSON.withAlpha(0.5) },
});
1. 点击选择实体
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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 要素并检查属性
import { Cesium3DTileFeature, Color } from "cesium";
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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. 拾取地形位置(根据点击获取经纬度)
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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);
对于 3D 内容的高度,请改用 scene.pickPosition(见上文)。
4. 使用 drillPick 多重拾取
import { EntityCollection, CallbackProperty, ColorMaterialProperty, Color } from "cesium";
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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. 使用 MOUSE_MOVE 高亮悬停对象
import { Color } from "cesium";
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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);
对于实体,拾取对象是 picked.id;应替换图形材质,而不是替换 picked.color(例如,保存 picked.id.polygon.material 并将其设为 Color.YELLOW,同时先恢复上一个实体的材质)。请构建提示词指定的图形类型:“区域上的多边形”表示使用 polygon 图形(位置通过 PolygonHierarchy 指定),而非 rectangle 图形,即使该区域呈矩形也一样。
6. 基于拖动的绘制和测量
import { Cartographic, EllipsoidGeodesic, Ellipsoid, Color } from "cesium";
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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. 鼠标移动时显示坐标读数
请为标签初始化一个合理的起始值,使其在指针移动之前就可见——场景通常会在没有用户交互时断言默认读数。始终使用 CesiumMath.toDegrees 进行转换。
import { HorizontalOrigin, VerticalOrigin, Cartesian2, Cartesian3,
Cartographic, Math as CesiumMath } from "cesium";
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
// Seed position/text so the label renders immediately on load.
const initialLon = 25.0;
const initialLat = 37.5;
const coordLabel = viewer.entities.add({
position: Cartesian3.fromDegrees(initialLon, initialLat),
label: {
text: `Lon: ${initialLon.toFixed(2)} Lat: ${initialLat.toFixed(2)}`,
show: true, 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)}`;
}
}, ScreenSpaceEventType.MOUSE_MOVE);
8. 根据拾取对象类型执行条件行为
import { Cesium3DTileFeature } from "cesium";
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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 实现非阻塞悬停(v1.136+)
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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. 悬停 + 选中与轮廓(完整模式)
轮廓是应用于所选图元的后处理边缘检测效果。要让轮廓在截图中清晰可见,选中的对象必须是带深度的不透明图元(长方体、模型、3D Tiles 要素);在区域尺度的相机高度下,贴地的平面多边形和点很少能产生明显边缘。过去的评估中,边缘太细而无法辨认,导致结果不理想;在相同场景中为长方体使用更粗的轮廓边缘后,效果有所改善。为避免这种退步:
- 优先使用具有非零
dimensions的box图形(区域尺度的相机高度下,每边通常为 10–50 km),或使用 3D Tiles 要素,而不是point或贴地多边形。 - 轮廓
length至少设为 0.25;城市尺度或更广的取景范围应提高到0.5。低于 0.1 的值在评测使用的截图分辨率下几乎不可见。 - 选择与卫星影像高对比度的颜色(
Color.ORANGE、Color.YELLOW、Color.LIME);避免使用会与海洋影像融为一体的蓝色。
import { PostProcessStageLibrary, Color } from "cesium";
const scene = viewer.scene;
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
const silhouetteHover = PostProcessStageLibrary.createEdgeDetectionStage();
silhouetteHover.uniforms.color = Color.YELLOW;
silhouetteHover.uniforms.length = 0.25;
silhouetteHover.selected = [];
const silhouetteSelect = PostProcessStageLibrary.createEdgeDetectionStage();
silhouetteSelect.uniforms.color = Color.ORANGE;
silhouetteSelect.uniforms.length = 0.5;
silhouetteSelect.selected = [];
scene.postProcessStages.add(
PostProcessStageLibrary.createSilhouetteStage([silhouetteHover, silhouetteSelect]));
let selectedFeature;
handler.setInputAction((movement) => {
silhouetteHover.selected = [];
const picked = scene.pick(movement.endPosition);
if (defined(picked) && picked !== selectedFeature) {
silhouetteHover.selected = [picked];
}
}, ScreenSpaceEventType.MOUSE_MOVE);
handler.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);
对于加载时预先选中实体的场景(无需用户交互),添加实体后直接赋值 silhouetteSelect.selected = [entity],这样初始截图中就能看到轮廓。
11. 使用自定义逻辑进行滚轮缩放
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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. 右键上下文菜单
viewer.scene.canvas.addEventListener("contextmenu", (e) => e.preventDefault());
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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. 拖动交互(移动实体)
const handler = new ScreenSpaceEventHandler(viewer.scene.canvas);
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);
性能提示
- 在 MOUSE_MOVE 上优先使用
pickAsync,而不是pick——同步拾取会阻塞 GPU 管线;pickAsync会让出执行权给 GPU,并在下一帧解析(WebGL2,v1.136+)。 - 使用带
limit的drillPick——如果不设置限制,每个重叠对象都会触发一次场景重新渲染。 - 仅需点击拾取时,避免在 MOUSE_MOVE 中使用
pick——MOUSE_MOVE 会在指针每次移动时触发,并且每次都会触发拾取渲染通道。 - 启用
depthTestAgainstTerrain,以便在地形上准确获取pickPosition结果。 - 销毁未使用的处理器——每个处理器都会注册 DOM 监听器;若不清理,会造成内存泄漏。
- 对开销较大的悬停逻辑进行节流——对于简单高亮以外的操作,将防抖间隔设为 50–100 ms。
- 使用
pickPosition前检查scene.pickPositionSupported——在不受支持的 GPU 上改用camera.pickEllipsoid。 - 仅在需要时设置
scene.pickTranslucentDepth = true——这会增加一次额外的渲染通道。 - 复用结果对象——向
pickPosition传入临时Cartesian3,以避免 MOUSE_MOVE 期间产生 GC 压力。 - 配合拾取设置
scene.requestRenderMode = true,以避免不必要的渲染;仅在状态改变时调用scene.requestRender()。
另请参阅
- cesiumjs-entities——Entity API、图形类型、DataSources
- cesiumjs-3d-tiles——Cesium3DTileset、Cesium3DTileFeature、样式、元数据
- cesiumjs-camera——Camera.pickEllipsoid、ScreenSpaceCameraController、flyTo