mcpField
C
cesiumjs-interaction

cesiumjs-interaction

github.com/cesiumgs开发193241 次安装6 周前更新
代码仓库

关于 cesiumjs-interaction

面向 CesiumJS 开发者的交互实现参考技能,涵盖事件处理、场景拾取、坐标读数、悬停效果及地理场景的交互模式。

主要功能

  1. 01屏幕空间鼠标与触摸事件
  2. 02同步及异步拾取
  3. 03检查 3D Tiles 要素属性
  4. 04读数所需的坐标转换
  5. 05悬停高亮与选择

适用场景

  1. 01点击选择实体
  2. 02检查拾取到的 3D Tiles 要素
  3. 03指针移动时显示经纬度
  4. 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 实例
Cesium3DTileFeatureCesium3DTileFeature.getProperty(name)、.getPropertyIds()、.color
Billboard/Label(集合){ primitive, id }id 是用户设置的 id
Primitive(几何体){ primitive, id }id 是 GeometryInstance 的 id
Globe 表面undefined使用 camera.pickEllipsoid() 或 pickPosition()

交互演示的相机取景

拾取和悬停演示必须渲染主体地理区域,而不是星空。近期出现的问题是相机朝向天空(俯仰角约为 -45° 或更浅),或者影像加载失败,导致场景一片漆黑。以下三条规则不可妥协:

  1. 在需要查看区域地图的场景中,使用 viewer.camera.setView(或设置 duration: 0 的 flyTo),并明确将俯仰角设为 CesiumMath.toRadians(-90)(天底方向)。不要依赖默认俯仰角。
  2. 将场景取景在足以容纳所有主体要素的高度。 城市级演示使用 50–200 km;州/区域级使用 1–3 Mm;多州或群岛级使用 1.5–4 Mm。如果图钉或多边形落在取景框外,即使程序检查通过,评审也会降低候选方案的评分。
  3. 区域图钉演示必须完整包含纬度范围,并留出余量。 查看西海岸的西雅图/旧金山/洛杉矶时,中心点需靠近 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);

性能提示

  1. 在 MOUSE_MOVE 上优先使用 pickAsync,而不是 pick——同步拾取会阻塞 GPU 管线;pickAsync 会让出执行权给 GPU,并在下一帧解析(WebGL2,v1.136+)。
  2. 使用带 limit 的 drillPick——如果不设置限制,每个重叠对象都会触发一次场景重新渲染。
  3. 仅需点击拾取时,避免在 MOUSE_MOVE 中使用 pick——MOUSE_MOVE 会在指针每次移动时触发,并且每次都会触发拾取渲染通道。
  4. 启用 depthTestAgainstTerrain,以便在地形上准确获取 pickPosition 结果。
  5. 销毁未使用的处理器——每个处理器都会注册 DOM 监听器;若不清理,会造成内存泄漏。
  6. 对开销较大的悬停逻辑进行节流——对于简单高亮以外的操作,将防抖间隔设为 50–100 ms。
  7. 使用 pickPosition 前检查 scene.pickPositionSupported——在不受支持的 GPU 上改用 camera.pickEllipsoid。
  8. 仅在需要时设置 scene.pickTranslucentDepth = true——这会增加一次额外的渲染通道。
  9. 复用结果对象——向 pickPosition 传入临时 Cartesian3,以避免 MOUSE_MOVE 期间产生 GC 压力。
  10. 配合拾取设置 scene.requestRenderMode = true,以避免不必要的渲染;仅在状态改变时调用 scene.requestRender()。

另请参阅

  • cesiumjs-entities——Entity API、图形类型、DataSources
  • cesiumjs-3d-tiles——Cesium3DTileset、Cesium3DTileFeature、样式、元数据
  • cesiumjs-camera——Camera.pickEllipsoid、ScreenSpaceCameraController、flyTo

评论

支持 Markdown