Vue3 + Cesium 鼠标交互绘制多边形 / 线(贴地绘制,动态预览,Entity 实现)

前言

上一篇我们是写死经纬度添加 Entity;业务中经常需要鼠标在地图上直接圈选绘制围栏、轨迹 。

Cesium 实现动态绘制核心两个关键点:

  1. ScreenSpaceEventHandler:监听鼠标左击、移动、右击事件
  2. CallbackProperty:回调属性,实时返回坐标数组,实现鼠标拖动时图形实时刷新预览(重点!普通属性赋值不会动态更新)稀土掘金

交互规则(和行业标绘工具保持一致)

  • ✅ 左键单击:添加顶点
  • ✅ 鼠标移动:实时预览橡皮筋线 / 面
  • ✅ 右键单击:结束当前绘制,生成最终 Entity
  • ✅ 支持贴地形 clampToGround: true
  • ✅ 绘制过程可见顶点标记点

两种绘制模式:POLYLINE 折线 / POLYGON 多边形

完整组件代码

复制代码
<template>
  <div class="cesium-wrap">
    <div class="btn-group">
      <button @click="startDraw('POLYLINE')">开始绘制折线</button>
      <button @click="startDraw('POLYGON')">开始绘制多边形</button>
      <button @click="stopDraw">取消绘制</button>
      <button @click="clearAll">清空所有图形</button>
    </div>
    <div class="cesium-container" ref="cesiumRef"></div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import * as Cesium from 'cesium'
import 'cesium/Build/Cesium/Widgets/widgets.css'

const cesiumRef = ref(null)
let viewer = null
let handler = null // 鼠标事件管理器

// 绘制状态
let drawMode = '' // POLYLINE / POLYGON
let drawingPoints = [] // 收集笛卡尔顶点
let tempPreviewEntity = null // 临时预览图形(橡皮筋)
let tempPointEntities = [] // 顶点标记点

// 初始化地图
function initViewer() {
  viewer = new Cesium.Viewer(cesiumRef.value, {
    animation: false,
    timeline: false,
    baseLayerPicker: false,
    geocoder: false,
    homeButton: false,
    fullscreenButton: false,
    navigationHelpButton: false
  })
  viewer.camera.flyTo({
    destination: Cesium.Cartesian3.fromDegrees(108.95, 34.27, 80000)
  })
  handler = new Cesium.ScreenSpaceEventHandler(viewer.scene.canvas)
  bindMouseEvent()
}

// 绑定鼠标事件
function bindMouseEvent() {
  // 左键点击:添加顶点
  handler.setInputAction((e) => {
    if (!drawMode) return
    // 拾取地面坐标(贴地)
    const ray = viewer.camera.getPickRay(e.position)
    const cartesian = viewer.scene.globe.pick(ray, viewer.scene)
    if (!Cesium.defined(cartesian)) return

    drawingPoints.push(cartesian)
    // 添加顶点小圆点
    const pointEntity = viewer.entities.add({
      position: cartesian,
      point: {
        pixelSize: 8,
        color: Cesium.Color.YELLOW,
        outlineColor: Cesium.Color.BLACK,
        outlineWidth: 1
      }
    })
    tempPointEntities.push(pointEntity)

    // 第一次点击,初始化预览图形
    if (!tempPreviewEntity) {
      createPreviewEntity()
    }
  }, Cesium.ScreenSpaceEventType.LEFT_CLICK)

  // 鼠标移动:实时更新预览
  handler.setInputAction((e) => {
    if (!drawMode || !tempPreviewEntity || drawingPoints.length === 0) return
    const ray = viewer.camera.getPickRay(e.endPosition)
    const mouseCartesian = viewer.scene.globe.pick(ray, viewer.scene)
    if (!Cesium.defined(mouseCartesian)) return
    // 存到临时变量给Callback读取
    tempPreviewEntity._mousePos = mouseCartesian
  }, Cesium.ScreenSpaceEventType.MOUSE_MOVE)

  // 右键点击:结束绘制
  handler.setInputAction(() => {
    if (!drawMode || drawingPoints.length < 2) {
      cancelDraw()
      return
    }
    finishDraw()
  }, Cesium.ScreenSpaceEventType.RIGHT_CLICK)
}

// 创建预览橡皮筋图形(CallbackProperty动态驱动)
function createPreviewEntity() {
  if (tempPreviewEntity) return
  tempPreviewEntity = viewer.entities.add({
    polyline: {
      positions: new Cesium.CallbackProperty(() => {
        const pts = [...drawingPoints]
        if (tempPreviewEntity._mousePos) {
          pts.push(tempPreviewEntity._mousePos)
        }
        return pts
      }, false),
      width: 3,
      material: Cesium.Color.CYAN,
      clampToGround: true
    },
    polygon: drawMode === 'POLYGON' ? {
      hierarchy: new Cesium.CallbackProperty(() => {
        const pts = [...drawingPoints]
        if (tempPreviewEntity._mousePos) {
          pts.push(tempPreviewEntity._mousePos)
        }
        return new Cesium.PolygonHierarchy(pts)
      }, false),
      material: Cesium.Color.CYAN.withAlpha(0.25),
      outline: true,
      outlineColor: Cesium.Color.CYAN,
      clampToGround: true
    } : undefined
  })
}

// 开始绘制
function startDraw(type) {
  cancelDraw()
  drawMode = type
}

// 结束绘制,生成正式实体
function finishDraw() {
  // 移除预览橡皮筋
  viewer.entities.remove(tempPreviewEntity)
  tempPreviewEntity = null

  if (drawMode === 'POLYLINE') {
    viewer.entities.add({
      name: '绘制折线',
      polyline: {
        positions: drawingPoints,
        width: 3,
        material: Cesium.Color.GREEN,
        clampToGround: true
      }
    })
  } else if (drawMode === 'POLYGON') {
    viewer.entities.add({
      name: '绘制多边形',
      polygon: {
        hierarchy: new Cesium.PolygonHierarchy(drawingPoints),
        material: Cesium.Color.GREEN.withAlpha(0.3),
        outline: true,
        outlineColor: Cesium.Color.GREEN,
        clampToGround: true
      }
    })
  }
  // 清除顶点标记、重置状态
  clearTempPoints()
  resetDrawState()
}

// 取消绘制
function stopDraw() {
  cancelDraw()
}
function cancelDraw() {
  if (tempPreviewEntity) {
    viewer.entities.remove(tempPreviewEntity)
    tempPreviewEntity = null
  }
  clearTempPoints()
  resetDrawState()
}
function clearTempPoints() {
  tempPointEntities.forEach(p => viewer.entities.remove(p))
  tempPointEntities = []
}
function resetDrawState() {
  drawMode = ''
  drawingPoints = []
}

// 清空所有
function clearAll() {
  viewer.entities.removeAll()
  resetDrawState()
}

onMounted(() => {
  initViewer()
})
onUnmounted(() => {
  handler?.destroy()
  viewer?.destroy()
})
</script>

<style scoped>
.cesium-wrap {
  width: 100%;
  height: 100vh;
}
.btn-group {
  padding: 10px;
  background: #fff;
  z-index: 9;
  position: relative;
}
button {
  margin: 0 4px;
  padding: 6px 10px;
}
.cesium-container {
  width: 100%;
  height: calc(100vh - 50px);
}
</style>

核心原理讲解

1. CallbackProperty 为什么必须用?

普通写法 positions: pts 只是赋值一次,后续修改数组图形不会刷新。

CallbackProperty(fn, false):每一帧渲染时都会执行回调函数读取最新坐标,实现鼠标移动实时预览橡皮筋效果

第二个参数 isConstant:false = 动态变化;true = 常量

2. 拾取坐标两种方式区别

复制代码
// 方式1:globe.pick → 拾取地形表面(贴地绘制,本文用这个)
const ray = viewer.camera.getPickRay(e.position)
const cartesian = viewer.scene.globe.pick(ray, viewer.scene)

// 方式2:viewer.scene.pickPosition(e.position) → 拾取模型、3dtiles、地形都可以,但是依赖深度缓冲
const cartesian = viewer.scene.pickPosition(e.position)
  • 标绘围栏、地面轨迹优先 globe.pick,稳定,不容易受 3D 瓦片深度异常影响

3. PolygonHierarchy

多边形面必须包一层 new Cesium.PolygonHierarchy(points),支持挖洞(内环),基础绘制只传外层点位即可。

常见坑(重点)

  1. 绘制的时候地球无法旋转拖拽
    绘制状态下 Cesium 默认相机操作和鼠标事件冲突;如果需要绘制时还能平移地图:可以加一个开关,绘制时关闭相机,或者用Ctrl + 鼠标拖拽方案,自行扩展。
  2. clampToGround 贴地面不生效
    • polygon 贴地:clampToGround:true
    • 贴地面不能设置 extrudedHeight 拉伸高度,互斥!
  3. 右键不生效、没反应
    确认 ScreenSpaceEventType.RIGHT_CLICK,不要和浏览器原生右键菜单冲突;生产环境可以加 e.preventDefault() 阻止浏览器右键菜单。
  4. 页面销毁内存泄漏
    onUnmounted 一定要销毁 handler.destroy() + viewer.destroy(),否则多次进入页面会叠加多个鼠标监听。
  5. CallbackProperty 不要写复杂逻辑
    回调每帧执行,里面不要写请求、循环、大量计算,容易掉帧。

扩展方向(可以继续迭代)

  1. 绘制矩形、圆形(鼠标按下拖动,松开完成,不是多点点击)
  2. 绘制完成后编辑顶点:拖拽顶点修改形状
  3. 绘制带面积、距离测量标签
  4. 绘制 GeoJSON 导出、导入
  5. 自定义材质:流动线、虚线、发光围栏
  6. 增加撤销、回退上一个顶点(回退键)
相关推荐
小幂同学1 天前
鼠标左右键自动点击怎么设置?单击、双击与宏录制说明
计算机外设·鼠标连点器·连点器·点击器·自动点击器
摩尔芯创1 天前
一期一会 | 什么是抬头显示器(HUD)?
计算机外设
苏州福兰克1 天前
运行裸眼3D显示器对电脑显卡配置有什么要求?
3d·计算机外设·电脑
巧手打字通-一起来打字1 天前
怎么才能科学快速的学会电脑26键盘打字?
计算机外设·电脑·键盘
苏州福兰克4 天前
没有3D内容,裸眼3D显示器还能用吗?
3d·计算机外设
豪气的程序猿4 天前
Windows 11 外接键盘偶尔漏键?用键盘小鹿练习前先做双设备对照
计算机外设·电脑
苏州福兰克4 天前
长期看裸眼3D显示器容易视觉疲劳吗?
3d·计算机外设
苏州福兰克4 天前
商用裸眼3D显示器和家用款主要差别在哪里?
3d·计算机外设
Rudon滨海渔村4 天前
台式机Win10实现“音量+”和“音量-”,键盘映射方案
windows·计算机外设·powertoys